feat: add optimistic locking for bttask and error classification

Version column in tasks table with WHERE id=? AND version=? guard.
Conflict detection in TaskBoardTab. error-classifier.ts: 6 error types
with actionable messages and retry logic. UsageMeter.svelte.
This commit is contained in:
Hibryda 2026-03-12 04:57:29 +01:00
parent 0fe43de357
commit 3cb65fd5e5
10 changed files with 763 additions and 32 deletions

27
bttask
View file

@ -98,6 +98,7 @@ def init_db():
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
version INTEGER DEFAULT 1,
FOREIGN KEY (assigned_to) REFERENCES agents(id),
FOREIGN KEY (created_by) REFERENCES agents(id)
);
@ -117,6 +118,14 @@ def init_db():
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
""")
# Migration: add version column if missing (for existing databases)
cursor = db.execute("PRAGMA table_info(tasks)")
columns = [row[1] for row in cursor.fetchall()]
if 'version' not in columns:
db.execute("ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1")
db.commit()
db.commit()
db.close()
@ -414,11 +423,20 @@ def cmd_status(args):
return
old_status = task['status']
db.execute(
"UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?",
(new_status, task['id'])
current_version = task['version'] if task['version'] is not None else 1
cursor = db.execute(
"UPDATE tasks SET status = ?, version = version + 1, updated_at = datetime('now') "
"WHERE id = ? AND version = ?",
(new_status, task['id'], current_version)
)
if cursor.rowcount == 0:
print(f"{C_RED}Error: Task was modified by another agent (version conflict).{C_RESET}")
print(f"{C_DIM}Re-fetch the task and try again.{C_RESET}")
db.close()
sys.exit(1)
# Auto-add comment for status change
comment_id = str(uuid.uuid4())
db.execute(
@ -429,7 +447,8 @@ def cmd_status(args):
db.commit()
db.close()
print(f"{C_GREEN}✓ [{short_id(task['id'])}] {format_state(old_status)} → {format_state(new_status)}{C_RESET}")
new_version = current_version + 1
print(f"{C_GREEN}✓ [{short_id(task['id'])}] {format_state(old_status)} → {format_state(new_status)} {C_DIM}(v{new_version}){C_RESET}")
def cmd_comment(args):