gateway
This commit is contained in:
+33
-511
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -19,484 +21,36 @@ import (
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFiles embed.FS
|
||||
|
||||
type migration struct {
|
||||
name string
|
||||
sql string
|
||||
}
|
||||
|
||||
var migrations = []migration{{"create-core-tables", `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
login TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
git_name TEXT NOT NULL DEFAULT '',
|
||||
git_email TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
func loadMigrations() ([]migration, error) {
|
||||
entries, err := migrationFiles.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
repo_path TEXT NOT NULL UNIQUE,
|
||||
default_branch TEXT NOT NULL,
|
||||
remote TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(login),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
migrations := []migration{}
|
||||
for _, e := range entries {
|
||||
num, name, ok := strings.Cut(strings.TrimSuffix(e.Name(), ".sql"), "-")
|
||||
_, err = strconv.Atoi(num)
|
||||
if !ok || len(num) != 3 || err != nil {
|
||||
return nil, fmt.Errorf("migration file %s: want NNN-name.sql", e.Name())
|
||||
}
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
title TEXT NOT NULL,
|
||||
branch TEXT NOT NULL,
|
||||
worktree_path TEXT NOT NULL,
|
||||
base_ref TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(login),
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS tasks_project ON tasks(project_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
task_id TEXT,
|
||||
actor TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`}, {"add-agent-sessions", `
|
||||
ALTER TABLE events ADD COLUMN agent_session_id TEXT;
|
||||
|
||||
CREATE TABLE agent_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
vendor TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
vendor_session_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(login),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX agent_sessions_task ON agent_sessions(task_id, created_at);
|
||||
CREATE INDEX agent_sessions_state ON agent_sessions(state);
|
||||
`}, {"add-inbox", `
|
||||
CREATE TABLE inbox_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
|
||||
thread_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL,
|
||||
blocking INTEGER NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
snoozed_until TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
resolved_by TEXT REFERENCES users(login),
|
||||
resolved_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX inbox_items_open ON inbox_items(state, priority, created_at);
|
||||
CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
|
||||
|
||||
CREATE TABLE inbox_responses (
|
||||
id TEXT PRIMARY KEY,
|
||||
item_id TEXT NOT NULL REFERENCES inbox_items(id),
|
||||
user_login TEXT NOT NULL REFERENCES users(login),
|
||||
type TEXT NOT NULL,
|
||||
option TEXT,
|
||||
text TEXT,
|
||||
attachments TEXT NOT NULL,
|
||||
snooze_until TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
delivered_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX inbox_responses_item ON inbox_responses(item_id, created_at);
|
||||
`}, {"index-events-by-task", `
|
||||
CREATE INDEX events_task ON events(task_id, seq);
|
||||
`}, {"add-turn-queue", `
|
||||
CREATE TABLE turn_queue (
|
||||
session_id TEXT PRIMARY KEY REFERENCES agent_sessions(id),
|
||||
actor TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`}, {"merge-response-types-into-other", `
|
||||
UPDATE inbox_responses SET type = 'other' WHERE type IN ('free_form', 'redirect', 'question');
|
||||
UPDATE inbox_items SET state = 'answered' WHERE state = 'redirected';
|
||||
`}, {"flatten-report-payloads-to-text", `
|
||||
UPDATE inbox_items SET payload = json_object('text', coalesce((SELECT group_concat('- ' || value, char(10)) FROM json_each(inbox_items.payload, '$.done')), ''))
|
||||
WHERE kind = 'report' AND json_type(payload, '$.done') IS NOT NULL;
|
||||
`}, {"rename-completed-tasks-to-discarded", `
|
||||
ALTER TABLE tasks RENAME COLUMN completed_at TO closed_at;
|
||||
ALTER TABLE tasks ADD COLUMN merge_commit TEXT NOT NULL DEFAULT '';
|
||||
UPDATE tasks SET status = 'discarded' WHERE status = 'completed';
|
||||
`}, {"dismiss-inbox-of-closed-tasks", `
|
||||
UPDATE inbox_items SET state = 'dismissed', resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), snoozed_until = NULL
|
||||
WHERE state = 'open' AND task_id IN (SELECT id FROM tasks WHERE status != 'active');
|
||||
`}, {"add-blobs-and-queued-images", `
|
||||
CREATE TABLE IF NOT EXISTS blobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
content_type TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(login),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE turn_queue_next (
|
||||
session_id TEXT PRIMARY KEY REFERENCES agent_sessions(id),
|
||||
actor TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
images TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO turn_queue_next (session_id, actor, text, created_at) SELECT session_id, actor, text, created_at FROM turn_queue;
|
||||
DROP TABLE turn_queue;
|
||||
ALTER TABLE turn_queue_next RENAME TO turn_queue;
|
||||
`}, {"add-task-merge-title", `
|
||||
ALTER TABLE tasks ADD COLUMN merge_title TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-web-push", `
|
||||
ALTER TABLE users ADD COLUMN notification_prefs TEXT NOT NULL
|
||||
DEFAULT '{"blocking_questions":true,"questions":false,"reports":false,"errors":false,"rate_limits":false}';
|
||||
|
||||
CREATE TABLE vapid_keys (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
public_key TEXT NOT NULL,
|
||||
private_key TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE push_subscriptions (
|
||||
endpoint TEXT PRIMARY KEY,
|
||||
user_login TEXT NOT NULL REFERENCES users(login),
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
subscriber TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`}, {"add-snapshots", `
|
||||
CREATE TABLE snapshots (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
|
||||
n INTEGER NOT NULL,
|
||||
commit_sha TEXT NOT NULL,
|
||||
tree_sha TEXT NOT NULL,
|
||||
event_seq INTEGER NOT NULL,
|
||||
trigger TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE (task_id, n)
|
||||
);
|
||||
`}, {"inline-question-choices", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN options TEXT;
|
||||
UPDATE inbox_responses SET options = json_array(option) WHERE option IS NOT NULL;
|
||||
ALTER TABLE inbox_responses DROP COLUMN option;
|
||||
|
||||
UPDATE inbox_items SET payload = json_set(json_remove(payload, '$.recommended_option'),
|
||||
'$.context', json_extract(payload, '$.context') || char(10) || char(10) || (
|
||||
SELECT group_concat(CASE WHEN o.value = json_extract(inbox_items.payload, '$.recommended_option') THEN '- (*) ' ELSE '- ( ) ' END || o.value, char(10))
|
||||
FROM json_each(inbox_items.payload, '$.options') o),
|
||||
'$.multiple', json('false'),
|
||||
'$.recommended', json_array(json_extract(payload, '$.recommended_option')))
|
||||
WHERE kind = 'question' AND json_type(payload, '$.recommended_option') IS NOT NULL;
|
||||
`}, {"unsnooze-inbox-items", `
|
||||
UPDATE inbox_items SET snoozed_until = NULL WHERE snoozed_until IS NOT NULL;
|
||||
`}, {"add-task-todo-item", `
|
||||
ALTER TABLE tasks ADD COLUMN todo_item TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-lead-agents", `
|
||||
CREATE TABLE agent_sessions_next (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT REFERENCES tasks(id),
|
||||
owner TEXT NOT NULL DEFAULT '',
|
||||
vendor TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
vendor_session_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(login),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO agent_sessions_next (id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at)
|
||||
SELECT id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at FROM agent_sessions;
|
||||
DROP TABLE agent_sessions;
|
||||
ALTER TABLE agent_sessions_next RENAME TO agent_sessions;
|
||||
CREATE INDEX agent_sessions_task ON agent_sessions(task_id, created_at);
|
||||
CREATE INDEX agent_sessions_state ON agent_sessions(state);
|
||||
CREATE INDEX agent_sessions_owner ON agent_sessions(owner, created_at);
|
||||
|
||||
ALTER TABLE events ADD COLUMN owner TEXT;
|
||||
CREATE INDEX events_owner ON events(owner, seq) WHERE owner IS NOT NULL;
|
||||
|
||||
ALTER TABLE users ADD COLUMN focus TEXT NOT NULL DEFAULT '{"tabs":["transcript"],"tab":"transcript"}';
|
||||
|
||||
CREATE TABLE lead_inputs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner TEXT NOT NULL REFERENCES users(login),
|
||||
kind TEXT NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
images TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX lead_inputs_owner ON lead_inputs(owner, id);
|
||||
`}, {"add-focus-project", `
|
||||
UPDATE users SET focus = json_set(focus, '$.project_id', (SELECT project_id FROM tasks WHERE id = json_extract(users.focus, '$.task_id')))
|
||||
WHERE json_extract(focus, '$.task_id') IS NOT NULL;
|
||||
DELETE FROM lead_inputs WHERE text LIKE 'The human switched the right pane %' OR text = 'The human cleared the right pane.';
|
||||
`}, {"add-task-merging-by", `
|
||||
ALTER TABLE tasks ADD COLUMN merging_by TEXT NOT NULL DEFAULT '';
|
||||
`}, {"flag-handoff-prompts", `
|
||||
UPDATE events SET payload = json_set(payload, '$.handoff', json('true'))
|
||||
WHERE kind = 'user.message' AND actor = 'byre' AND json_extract(payload, '$.text') LIKE 'Write a handoff for a fresh %';
|
||||
`}, {"rename-todo-tab-to-project", `
|
||||
UPDATE users SET focus = json_set(focus,
|
||||
'$.tab', CASE json_extract(focus, '$.tab') WHEN 'todo' THEN 'project' ELSE json_extract(focus, '$.tab') END,
|
||||
'$.tabs', json((SELECT json_group_array(CASE value WHEN 'todo' THEN 'project' ELSE value END) FROM json_each(users.focus, '$.tabs'))));
|
||||
`}, {"add-snapshot-forks", `
|
||||
ALTER TABLE tasks ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN forked_at INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE agent_sessions ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE agent_sessions ADD COLUMN forked_at TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-task-port-base", `
|
||||
ALTER TABLE tasks ADD COLUMN port_base INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE tasks SET port_base = (
|
||||
SELECT 41000 + 10 * (count(*) - 1) FROM tasks AS earlier
|
||||
WHERE earlier.status = 'active' AND (earlier.created_at < tasks.created_at OR (earlier.created_at = tasks.created_at AND earlier.id <= tasks.id))
|
||||
) WHERE status = 'active' AND port_base = 0;
|
||||
`}, {"add-task-merge-queue", `
|
||||
ALTER TABLE tasks ADD COLUMN merge_queued_by TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN merge_queued_at TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-task-previews", `
|
||||
CREATE TABLE previews (
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
name TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (task_id, name),
|
||||
UNIQUE (task_id, port)
|
||||
);
|
||||
`}, {"add-task-color", `
|
||||
ALTER TABLE tasks ADD COLUMN color INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE tasks SET color = (SELECT count(*) FROM tasks AS earlier WHERE earlier.rowid < tasks.rowid) % 11;
|
||||
UPDATE tasks SET color = (
|
||||
SELECT count(*) FROM tasks AS earlier WHERE earlier.status = 'active' AND earlier.rowid < tasks.rowid
|
||||
) % 11 WHERE status = 'active';
|
||||
`}, {"drop-turn-queue", `
|
||||
UPDATE agent_sessions SET state = 'idle' WHERE state = 'queued';
|
||||
DROP TABLE turn_queue;
|
||||
`}, {"add-snapshot-reviews", `
|
||||
CREATE TABLE reviews (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
snapshot_n INTEGER NOT NULL,
|
||||
agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
|
||||
vendor TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
result TEXT NOT NULL DEFAULT '',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
finished_at TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE (task_id, snapshot_n)
|
||||
);
|
||||
CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
|
||||
`}, {"add-task-inputs", `
|
||||
CREATE TABLE task_inputs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
actor TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
images TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX task_inputs_task ON task_inputs(task_id, id);
|
||||
`}, {"drop-web-push", `
|
||||
DROP TABLE push_subscriptions;
|
||||
DROP TABLE vapid_keys;
|
||||
`}, {"delete-report-inbox-items", `
|
||||
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'report');
|
||||
DELETE FROM inbox_items WHERE kind = 'report';
|
||||
`}, {"allow-sessionless-reviews", `
|
||||
CREATE TABLE reviews_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
snapshot_n INTEGER NOT NULL,
|
||||
agent_session_id TEXT REFERENCES agent_sessions(id),
|
||||
vendor TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
result TEXT NOT NULL DEFAULT '',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
finished_at TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE (task_id, snapshot_n)
|
||||
);
|
||||
INSERT INTO reviews_new SELECT * FROM reviews;
|
||||
DROP TABLE reviews;
|
||||
ALTER TABLE reviews_new RENAME TO reviews;
|
||||
CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
|
||||
`}, {"add-workflows", `
|
||||
CREATE TABLE workflows (
|
||||
name TEXT PRIMARY KEY,
|
||||
definition TEXT NOT NULL
|
||||
);
|
||||
ALTER TABLE tasks ADD COLUMN workflow_name TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN workflow_definition TEXT NOT NULL DEFAULT '';
|
||||
`}, {"run-workflow-stages", `
|
||||
ALTER TABLE tasks ADD COLUMN workflow_brief TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE agent_sessions ADD COLUMN workflow_stage INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE agent_sessions ADD COLUMN stage_output TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE agent_sessions ADD COLUMN stage_finished INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE workflows SET definition = json_set(definition, '$.stages',
|
||||
(SELECT json_group_array(json_remove(value, '$.vendor')) FROM (SELECT value FROM json_each(definition, '$.stages') ORDER BY key)));
|
||||
UPDATE tasks SET workflow_definition = json_set(workflow_definition, '$.stages',
|
||||
(SELECT json_group_array(json_remove(value, '$.vendor')) FROM (SELECT value FROM json_each(workflow_definition, '$.stages') ORDER BY key)))
|
||||
WHERE workflow_definition != '';
|
||||
`}, {"workflow-stage-tasks", `
|
||||
ALTER TABLE tasks ADD COLUMN workflow_run TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN workflow_stage TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN workflow_title TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN stage_output TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN stage_commit TEXT NOT NULL DEFAULT '';
|
||||
UPDATE tasks SET workflow_run = id, workflow_title = title,
|
||||
workflow_stage = coalesce(
|
||||
(SELECT json_extract(tasks.workflow_definition, '$.stages[' || (s.workflow_stage - 1) || '].name') FROM agent_sessions s
|
||||
WHERE s.task_id = tasks.id AND s.workflow_stage > 0 ORDER BY s.created_at DESC LIMIT 1),
|
||||
json_extract(workflow_definition, '$.stages[0].name'))
|
||||
WHERE workflow_name != '' AND workflow_run = '';
|
||||
`}, {"drop-preview-tab", `
|
||||
UPDATE users SET focus = json_set(focus,
|
||||
'$.tab', CASE json_extract(focus, '$.tab') WHEN 'preview' THEN 'transcript' ELSE json_extract(focus, '$.tab') END,
|
||||
'$.tabs', json((SELECT json_group_array(value) FROM json_each(users.focus, '$.tabs') WHERE value != 'preview')));
|
||||
UPDATE users SET focus = json_set(focus, '$.tabs', json('["transcript"]')) WHERE json_array_length(focus, '$.tabs') = 0;
|
||||
`}, {"drop-inbox-priority", `
|
||||
CREATE TABLE inbox_items_next (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
agent_session_id TEXT NOT NULL REFERENCES agent_sessions(id),
|
||||
thread_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
blocking INTEGER NOT NULL DEFAULT 1,
|
||||
payload TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
snoozed_until TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
resolved_by TEXT REFERENCES users(login),
|
||||
resolved_at TEXT
|
||||
);
|
||||
INSERT INTO inbox_items_next SELECT id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state, snoozed_until, created_at, resolved_by, resolved_at FROM inbox_items;
|
||||
DROP TABLE inbox_items;
|
||||
ALTER TABLE inbox_items_next RENAME TO inbox_items;
|
||||
CREATE INDEX inbox_items_open ON inbox_items(state, created_at);
|
||||
CREATE INDEX inbox_items_session ON inbox_items(agent_session_id, state);
|
||||
|
||||
CREATE TABLE users_next (
|
||||
login TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
git_name TEXT NOT NULL DEFAULT '',
|
||||
git_email TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
notification_prefs TEXT NOT NULL DEFAULT '{"questions":true,"reports":false,"errors":false,"rate_limits":false}',
|
||||
focus TEXT NOT NULL DEFAULT '{"tabs":["transcript"],"tab":"transcript"}'
|
||||
);
|
||||
INSERT INTO users_next (login, display_name, git_name, git_email, created_at, notification_prefs, focus)
|
||||
SELECT login, display_name, git_name, git_email, created_at,
|
||||
json_remove(json_set(notification_prefs, '$.questions', json(CASE WHEN json_extract(notification_prefs, '$.blocking_questions') THEN 'true' ELSE 'false' END)), '$.blocking_questions'),
|
||||
focus
|
||||
FROM users;
|
||||
DROP TABLE users;
|
||||
ALTER TABLE users_next RENAME TO users;
|
||||
`}, {"add-task-summary-and-discarding-by", `
|
||||
ALTER TABLE tasks ADD COLUMN summary TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN discarding_by TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-task-discard-state", `
|
||||
ALTER TABLE tasks ADD COLUMN discard_state TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN discard_session TEXT NOT NULL DEFAULT '';
|
||||
UPDATE tasks SET discard_state = 'held' WHERE discarding_by != '' AND status = 'active';
|
||||
`}, {"add-subtasks", `
|
||||
ALTER TABLE tasks ADD COLUMN parent_task_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN parent_merge_by TEXT NOT NULL DEFAULT '';
|
||||
CREATE INDEX tasks_parent ON tasks(parent_task_id) WHERE parent_task_id != '';
|
||||
`}, {"add-review-finding-states", `
|
||||
ALTER TABLE reviews ADD COLUMN finding_states TEXT NOT NULL DEFAULT '{}';
|
||||
UPDATE events SET payload = json_set(payload, '$.finding_states', json('{}'))
|
||||
WHERE kind IN ('review.started', 'review.finished') AND json_type(payload, '$.finding_states') IS NULL;
|
||||
`}, {"add-lead-notes", `
|
||||
CREATE TABLE lead_notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner TEXT NOT NULL REFERENCES users(login),
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX lead_notes_owner ON lead_notes(owner, created_at);
|
||||
`}, {"add-summary-reads", `
|
||||
CREATE INDEX events_summary ON events(task_id, seq) WHERE kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1;
|
||||
CREATE TABLE summary_reads (
|
||||
login TEXT NOT NULL REFERENCES users(login),
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id),
|
||||
seq INTEGER NOT NULL,
|
||||
PRIMARY KEY (login, task_id)
|
||||
);
|
||||
`}, {"add-task-finalizing", `
|
||||
ALTER TABLE tasks ADD COLUMN finalizing TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-task-state", `
|
||||
ALTER TABLE tasks ADD COLUMN state TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN review_requests INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE tasks SET state = ` + taskStateExpr + `;
|
||||
`}, {"add-task-finalizing-commit", `
|
||||
ALTER TABLE tasks ADD COLUMN finalizing_commit TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN finalizing_notes TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN finalizing_unchanged INTEGER NOT NULL DEFAULT 0;
|
||||
`}, {"add-task-merge-after-review", `
|
||||
ALTER TABLE tasks ADD COLUMN merge_after_review_by TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN merge_after_review_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN merge_after_review_target TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-inbox-response-images", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN images TEXT NOT NULL DEFAULT '[]';
|
||||
`}, {"add-lead-note-projects", `
|
||||
ALTER TABLE lead_notes ADD COLUMN project_id TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-session-subagents", `
|
||||
ALTER TABLE agent_sessions ADD COLUMN subagents INTEGER NOT NULL DEFAULT 0;
|
||||
`}, {"add-inbox-run-responses", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN client TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE inbox_responses ADD COLUMN exit_code INTEGER;
|
||||
`}, {"drop-client-runs", `
|
||||
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'client_run');
|
||||
DELETE FROM inbox_items WHERE kind = 'client_run';
|
||||
ALTER TABLE inbox_responses DROP COLUMN client;
|
||||
ALTER TABLE inbox_responses DROP COLUMN exit_code;
|
||||
`}, {"add-user-default-model", `
|
||||
ALTER TABLE users ADD COLUMN default_model TEXT NOT NULL DEFAULT 'claude-opus-5-5';
|
||||
`}, {"add-team-members", `
|
||||
ALTER TABLE agent_sessions ADD COLUMN member TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE task_inputs ADD COLUMN member TEXT NOT NULL DEFAULT '';
|
||||
`}, {"rename-inbox-tab", `
|
||||
UPDATE users SET focus = replace(focus, '"inbox"', '"questions"');
|
||||
`}, {"add-inbox-response-snapshots", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN snapshot_n INTEGER;
|
||||
`}, {"errors-to-transcript", `
|
||||
UPDATE events SET kind = 'session.error', payload = json_object('error', json_extract(payload, '$.payload.error'))
|
||||
WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'session_error';
|
||||
UPDATE events SET kind = 'session.rate_limited', payload = json_object(
|
||||
'vendor', json_extract(payload, '$.payload.vendor'),
|
||||
'rate_limit', json(json_extract(payload, '$.payload.rate_limit')),
|
||||
'error', json_extract(payload, '$.payload.error'))
|
||||
WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'rate_limited';
|
||||
DELETE FROM events WHERE kind IN ('inbox.created', 'inbox.updated') AND json_extract(payload, '$.kind') != 'question';
|
||||
UPDATE events SET payload = json_remove(payload, '$.kind') WHERE kind IN ('inbox.created', 'inbox.updated');
|
||||
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind != 'question');
|
||||
DELETE FROM inbox_items WHERE kind != 'question';
|
||||
ALTER TABLE inbox_items DROP COLUMN kind;
|
||||
`}}
|
||||
sql, err := migrationFiles.ReadFile("migrations/" + e.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
migrations = append(migrations, migration{name: name, sql: string(sql)})
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultPortFirst = 41000
|
||||
@@ -519,7 +73,7 @@ func Open(path string, portFirst int, onEvent func([]api.Event)) (*Store, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = migrate(db, migrations)
|
||||
err = migrate(db)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
@@ -528,8 +82,13 @@ func Open(path string, portFirst int, onEvent func([]api.Event)) (*Store, error)
|
||||
return &Store{db: db, path: path, portFirst: portFirst, onEvent: onEvent}, nil
|
||||
}
|
||||
|
||||
func migrate(db *sql.DB, migrations []migration) error {
|
||||
err := createMigrationsTable(db, migrations)
|
||||
func migrate(db *sql.DB) error {
|
||||
migrations, err := loadMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -553,43 +112,6 @@ func migrate(db *sql.DB, migrations []migration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMigrationsTable(db *sql.DB, migrations []migration) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
exists := 0
|
||||
err = tx.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = 'migrations'`).Scan(&exists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE TABLE migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version := 0
|
||||
err = tx.QueryRow(`PRAGMA user_version`).Scan(&version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range migrations[:min(version, len(migrations))] {
|
||||
_, err = tx.Exec(`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`, m.name, now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func appliedMigrations(db *sql.DB) (map[string]bool, error) {
|
||||
rows, err := db.Query(`SELECT name FROM migrations`)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user