Skip to content
Closed
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
23 changes: 12 additions & 11 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,8 +865,6 @@ class Event:
session_id TEXT
);

CREATE INDEX IF NOT EXISTS idx_tasks_session_id ON tasks(session_id);

CREATE TABLE IF NOT EXISTS task_links (
parent_id TEXT NOT NULL,
child_id TEXT NOT NULL,
Expand Down Expand Up @@ -943,7 +941,6 @@ class Event:
CREATE INDEX IF NOT EXISTS idx_links_parent ON task_links(parent_id);
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id, created_at);
CREATE INDEX IF NOT EXISTS idx_events_task ON task_events(task_id, created_at);
CREATE INDEX IF NOT EXISTS idx_events_run ON task_events(run_id, id);
CREATE INDEX IF NOT EXISTS idx_runs_task ON task_runs(task_id, started_at);
CREATE INDEX IF NOT EXISTS idx_runs_status ON task_runs(status);
CREATE INDEX IF NOT EXISTS idx_notify_task ON kanban_notify_subs(task_id);
Expand Down Expand Up @@ -1174,20 +1171,24 @@ def _migrate_add_optional_columns(conn: sqlite3.Connection) -> None:
_add_column_if_missing(
conn, "tasks", "session_id", "session_id TEXT"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_tasks_session_id "
"ON tasks(session_id)"
)

# Keep this outside SCHEMA_SQL: on existing boards CREATE TABLE IF NOT
# EXISTS is a no-op, so creating an index on a newly-added column before
# the additive migration runs crashes with "no such column: session_id".
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_tasks_session_id "
"ON tasks(session_id)"
)

# task_events gained a run_id column; back-fill it as NULL for
# historical events (they predate runs and can't be attributed).
ev_cols = {row["name"] for row in conn.execute("PRAGMA table_info(task_events)")}
if "run_id" not in ev_cols:
_add_column_if_missing(conn, "task_events", "run_id", "run_id INTEGER")
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_events_run "
"ON task_events(run_id, id)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_events_run "
"ON task_events(run_id, id)"
)

notify_table_exists = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='kanban_notify_subs'"
Expand Down
104 changes: 104 additions & 0 deletions tests/hermes_cli/test_kanban_db_init.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import sqlite3
import threading
from pathlib import Path

Expand Down Expand Up @@ -35,4 +36,107 @@ def worker() -> None:
assert errors == []
with kb.connect(board="default") as conn:
cols = {row["name"] for row in conn.execute("PRAGMA table_info(tasks)")}
indexes = {
row["name"]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='index'"
)
}
assert "max_retries" in cols
assert "session_id" in cols
assert "idx_tasks_session_id" in indexes
assert "idx_events_run" in indexes


def test_connect_migrates_existing_board_before_session_id_index(tmp_path):
db_path = tmp_path / "legacy-kanban.db"
conn = sqlite3.connect(db_path)
conn.executescript(
"""
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
assignee TEXT,
status TEXT NOT NULL,
priority INTEGER DEFAULT 0,
created_by TEXT,
created_at INTEGER NOT NULL,
started_at INTEGER,
completed_at INTEGER,
workspace_kind TEXT NOT NULL DEFAULT 'scratch',
workspace_path TEXT,
branch_name TEXT,
claim_lock TEXT,
claim_expires INTEGER,
tenant TEXT,
result TEXT,
idempotency_key TEXT,
consecutive_failures INTEGER NOT NULL DEFAULT 0,
worker_pid INTEGER,
last_failure_error TEXT,
max_runtime_seconds INTEGER,
last_heartbeat_at INTEGER,
current_run_id INTEGER,
workflow_template_id TEXT,
current_step_key TEXT,
skills TEXT,
model_override TEXT,
max_retries INTEGER
);
CREATE TABLE task_links (
parent_id TEXT NOT NULL,
child_id TEXT NOT NULL,
PRIMARY KEY (parent_id, child_id)
);
CREATE TABLE task_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
author TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE task_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
kind TEXT NOT NULL,
payload TEXT,
created_at INTEGER NOT NULL
);
CREATE TABLE task_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
profile TEXT,
step_key TEXT,
status TEXT NOT NULL,
claim_lock TEXT,
claim_expires INTEGER,
worker_pid INTEGER,
max_runtime_seconds INTEGER,
last_heartbeat_at INTEGER,
started_at INTEGER NOT NULL,
ended_at INTEGER,
outcome TEXT,
summary TEXT,
metadata TEXT,
error TEXT
);
"""
)
conn.close()
kb._INITIALIZED_PATHS.discard(str(db_path.resolve()))

conn = kb.connect(db_path)
try:
cols = {row["name"] for row in conn.execute("PRAGMA table_info(tasks)")}
indexes = {
row["name"]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='index'"
)
}
finally:
conn.close()

assert "session_id" in cols
assert "idx_tasks_session_id" in indexes