* Overhaul dashboard UX with two-pane rooms and live turn persistence - Two-pane room board (Discord-style list + detail) with sender role colors, inbox attention badges, and per-role left-border sectioning - Settings page: nickname + language extracted to /#/settings; sidebar freed up for mini stats (rooms, queue, inbox) and nav badges - react-markdown + remark-gfm for message body (tables, code, links, proper line breaks); unified timeline merges outputs and human messages - Live turn elapsed counter ticks every second; paused state preserved across service restarts via new progress_text column on paired_turns (migration 015) so in-flight context survives reboots - Batched /api/rooms-timeline endpoint with server-side stale-while- revalidate cache + gzip; cold ~3s -> warm ~10ms - Optimistic UI for outgoing messages with rollback on failure; nickname propagated to sender_name on send - Density sweep: tighter paddings, radii, font sizes; tokens align across panel/card/sidebar/timeline Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Strip redundant LIVE/intent/state labels from turn header The left dot already signals LIVE and the role chip already shows the turn role; the standalone LIVE label, intentKind em, and state pill were repeating that information. Keep only role chip, time, elapsed, and a "중단됨" label when paused. Also picks up Prettier-formatted long lines in i18n and backend files left over from the previous commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Convert sidebar to lucide-react icon rail and fix optimistic UI - Replace 168px text-label sidebar with 56px flush-left icon rail (lucide-react icons: Gauge/Inbox/Activity/MessageSquare/Clock/Settings) Discord/Slack pattern: rail against left edge, no centered shell margin - Optimistic UI now survives polling: pending messages tracked in a separate state and merged into the timeline render; an effect prunes pending entries once the server snapshot confirms them by content match - LIVE elapsed counter shows seconds at all granularities (formatLiveElapsed uses m+s/h+m+s instead of dropping seconds after 60s) - Refresh button shows spin animation while busy Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Make Rooms the default landing view Rooms is the primary surface (chat-style live agent flow), so reorder nav (Rooms first) and switch DEFAULT_VIEW from inbox to rooms. The topbar fallback label also changes from 'usage' to 'rooms'. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Submit room message on Enter, allow Shift+Enter for newline IME composition (event.nativeEvent.isComposing) is excluded so that mid-Hangul Enter does not trigger send. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Viewport-lock rooms view to remove double scroll The rooms view now fills the viewport (height: 100vh - topbar) with list and detail each scrolling in their own column. The page itself no longer scrolls when the detail thread grows long, so the wheel scrolls whichever column the cursor is over. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Flatten rooms view, single-line compose with icon send button Drop nested card chrome on rooms view: outer panel now has no padding, border, or background so the rail / list / detail sit flush against the page. Internal panes (rooms-list, rooms-detail) lose their own borders and rely on a single divider line for separation. Compose textarea is single-row by default (rows=1) and grows up to ~5 lines; the send button is icon-only (Send from lucide-react), fixed 36x36, pinned to the bottom of the detail pane. The redundant '메시지' h4 and the 세부 fold are removed — the room title in the header already establishes context. Detail pane is now a flex column where only the timeline scrolls; the head sticks at top and the compose pins at bottom (messenger pattern), so the input is always reachable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix rooms layout: viewport lock, restore panel tone, ensure thread scrolls - Lock body to viewport height (overflow:hidden) only when rooms view is active, via body:has(.view-stack.view-rooms). Section nav becomes static so the layout fits exactly in the viewport instead of being clipped under the sticky header. - Restore subtle panel background and border on .rooms-list and .rooms-detail so chat content has tonal contrast against the page bg (pure black was unreadable). Outer panel chrome stays flat. - Make the thread section the only flex:1 child of the pinned card so it actually scrolls when content overflows; everything else (head, meta, current turn, watcher fold, compose) stays flex:none. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Make rooms-detail itself scroll with sticky head and compose Drop the brittle flex-column-with-1fr-child setup; just let .rooms-detail be overflow-y:auto and pin .room-card-head to top and .room-compose-section to bottom via position:sticky. The head and compose stay visible while the thread between them scrolls naturally. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Match compose-section background to panel The sticky compose bar was using var(--bg) (near-black) which jarred against the surrounding panel tone. Use var(--panel) so it sits continuously with the room detail. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix compose full-width and hide internal IPC payloads - The compose section's full-width grid was being clobbered by the later .room-section { display: grid } rule (same specificity, source order won). Bump specificity to .room-section.room-compose-section with display: block so the inner form/textarea fills the panel. - Filter out internal protocol payloads from the timeline: isInternalProtocolPayload() detects {"author":..."recipient":...} shaped agent IPC messages and excludes them from humanMessages and outputEntries. These are routing metadata, not human-readable chat. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Tighten rooms width: explicit 100% on detail/card, responsive list - Make rooms-list responsive (clamp(180px, 16vw, 240px)) so on smaller screens the chat area gets more room - Add explicit width: 100% to rooms-twopane, rooms-detail, and the pinned room-card-v2 so nothing collapses to content-size Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove timeline left indent and broaden IPC payload filter - Drop the 3px colored left border + 12px left padding on each timeline item; messages were starting 29px from the panel edge (3 border + 12 item padding + 14 thread padding). Role colors are already conveyed by the role chip, so the border is redundant. - Tighten thread-section padding from 14px to 12px to match compose. - Broaden isInternalProtocolPayload to also catch <sub-agent-prompt>, <tool-call>, <internal> wrapped payloads — these were leaking into the visible thread when the agent emitted them as message content. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Restore role color borders and tighten left padding - Bring back the role-colored left border on timeline items (2px now, was 3px) so owner/reviewer/arbiter/human are visually distinct - Tighten left-side spacing across the chat panel: thread section padding-left 12 -> 4, item padding-left 12 -> 8, card head padding-left 14 -> 8, compose-section padding-left 12 -> 4. Content now starts ~10px from the detail panel's inner edge instead of ~30px Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Unclamp output text in unified timeline + add measure debug toggle - The legacy .room-output-item p / .room-message-item p / .room-latest-output p rules used -webkit-line-clamp:2/4 to truncate previews. The unified timeline doesn't use those classes so it shouldn't be clamped, but add a defensive override on .room-timeline-body .parsed-body that unclamps and clears max-height in case any sibling rule leaks in. - Add ?debug=1 (outline panels) and ?measure=1 (overlay container x/w in viewport coords) toggles via App.tsx so we can diagnose layout issues without npm-installing devtools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Tighten rooms layout left/right margins Drop view-panel and section-nav left margin from 12px to 8px. Removes the visible empty band between icon-rail and rooms-list (was 12px, now 8px). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove internal left padding in rooms-detail panel The detail panel had 4-8px of left padding inside the thread and compose sections, leaving an empty band on the left. Drop padding-left to 0 on .room-thread-section and .room-compose-section. Timeline items keep their own 8px padding-left (for the role border + text gap), and the textarea inherits the same flush-left starting position as messages — they align vertically now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix 26px left empty band: zero out .room-expanded padding in pinned mode Root cause located via pixel scan + DOM dump: the legacy .room-expanded wrapper (used by the click-to-expand layout) has padding: 14px 0 4px 26px to align expanded content with the head's pulse-dot column. In the pinned twopane layout it was leaving a 26px empty band between the panel border and the chat content. Override .room-expanded inside .room-card-v2.is-pinned to zero padding/margin/border-top/gap, so thread-section and compose-section align flush at the panel's inner left edge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add Playwright + dash-inspect helper script Playwright (chromium) is installed so future layout debugging can use real DOM/computed-style queries instead of pixel scans on screenshots. Helper commands: bun scripts/dash-inspect.ts shot [route] bun scripts/dash-inspect.ts measure '<selector>' bun scripts/dash-inspect.ts eval '<expr>' bun scripts/dash-inspect.ts dom '<selector>' bun scripts/dash-inspect.ts click '<selector>' Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Auto-scroll chat to bottom on send and on new messages - When the user sends a message, scroll the rooms-detail panel to the very bottom over 4 frames (covers the post-render layout settle). - Add a near-bottom tracker: a ref flips false when the user scrolls more than 80px from the bottom, true otherwise. The auto-follow effect (triggered when messages/outputs/pending counts change) only scrolls if the user was already near the bottom — so reading scrollback doesn't get yanked. - Initial room selection and any new content added while at-bottom always shows the latest message. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add Settings page: model role config + multi-account management Backend: - src/settings-store.ts: filesystem reader for Claude (~/.claude, ~/.claude-accounts/N) and Codex (~/.codex, ~/.codex-accounts/N) account directories. Returns safe metadata only (expiresAt, scopes, subscriptionType, planType, accountId from JWT) — never raw tokens. Atomic .env writer for OWNER/REVIEWER/ARBITER MODEL/EFFORT pairs. Account directory deletion (refuses index 0). Claude account add via pasted OAuth JWT (decodes exp/sub from payload, writes new ~/.claude-accounts/N/.credentials.json). - src/web-dashboard-server.ts: new endpoints GET /api/settings/accounts GET /api/settings/models PUT /api/settings/models (PATCH alias) POST /api/settings/accounts/claude (body: { token }) DELETE /api/settings/accounts/{provider}/{index} Frontend: - apps/dashboard/src/api.ts: typed clients for the new endpoints. - apps/dashboard/src/App.tsx: SettingsPanel split into 일반 / 모델 / 계정. Model section: editable owner/reviewer/arbiter model+effort fields with dirty tracking and a save button. Account section: per-provider list with delete buttons (default index 0 protected) and a token-paste field for adding a Claude account. Both sections expose a "스택 재시작" button that calls the existing /api/services/stack/actions endpoint with a confirmation prompt — changes only take effect after restart. UX scope: phase B (model edit + account delete) + phase D (account add via paste) per the recommendation; full OAuth flow not included. Restart is gated behind a confirm() since it kills running agents. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Format: Prettier follow-up on settings PR Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Show cron / external-trigger messages inline (separate from CI watcher) trusted_external_bot messages (Sentry Cron, GitHub webhook triggers, etc.) were lumped into the CI-watcher fold and hidden by default. Split them: WATCHER_RE now only matches actual CI status messages (Build/Deploy/Lint/Release/[CI]/[Watcher]/GitHub Actions on bot kind), so the watcher fold stays focused on CI noise. trusted_external_bot messages now get rendered inline in the unified timeline as cronMessages (sourceKind preserved). senderRoleClass returns 'role-cron' for senders matching cron/sentry/webhook so the chip and left border show in muted gray to visually distinguish from agent and human messages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Live progress fallback: scan TASK_STATUS messages for current turn only When paired_turns.progress_text is empty (codex runner doesn't always populate it in time), fall back to scanning the room's messages table for ones starting with the TASK_STATUS_MESSAGE_PREFIX (the invisible 3-char prefix used on Discord live-edit messages). Strip the prefix and the trailing elapsed indicator (e.g. "\n\n12초"), then render in the LIVE timeline entry. Filters: - timestamp must be >= turn.createdAt (don't show pre-turn history) - sender role must match the current turn's role (don't show a stale reviewer status when owner is now speaking) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Server fallback: derive live progressText from messages when column empty Keep paired_turns.progress_text as the primary source (populated by recordTurnProgress in MessageTurnController). When the column is empty on an active turn — happens for codex turns where the controller path isn't fully wired, or any path where ingestion lags — scan the messages table for the most recent TASK_STATUS_MESSAGE_PREFIX-prefixed message that belongs to the current turn (sender role match + timestamp >= turn.createdAt) and use that as progressText. This makes the dashboard converge with Discord without requiring the column to be reliably written: if Discord shows a live edit, the ingest pipeline writes it to messages, and we surface it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Poll room timelines every 1.5s on rooms view (was every 15s) The rooms-detail thread was reusing the 15s overview refresh tick, which made the chat lag noticeably behind Discord. Add a dedicated setInterval(1500ms) fetch on the rooms view (cleared when leaving), with an in-flight guard so overlapping ticks don't pile up. Backend already has a 2s stale-while-revalidate cache so this stays cheap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Real-time rooms updates via Server-Sent Events Backend: GET /api/stream emits text/event-stream with 'rooms-timeline' events whenever the server's roomsTimelineCache rebuilds (key change). A 1.5s tick checks the cache; if builtAt advanced, push the new JSON, otherwise send a SSE comment heartbeat ': ping ts\n\n' to keep proxies from idle-closing the connection. AbortSignal handler closes the ReadableStream when the client disconnects. Frontend: replace setInterval(1500ms) on rooms view with EventSource. Initial fetch primes the data so first paint isn't blank, then SSE takes over for instant updates. EventSource auto-reconnects on disconnect; while it's flapping, a 2s polling fallback runs so data keeps flowing. When SSE recovers (next 'rooms-timeline' event) the polling fallback is cleared. Latency: previously 0–1.5s polling lag; now ~tick latency (50–150ms) once cache rebuilds. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add breathing room between timeline header and body Increase room-timeline-item gap from 2px to 6px so the role chip / timestamp line has visible breathing room above the parsed body — header → body relationship reads more clearly. Bump vertical padding from 6px to 8px and item separator from 1px to 2px to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Hide the 2px processing bar in pinned twopane to free role-color borders .room-card-v2.status-processing::before draws a 2px green bar at the card's left edge. In pinned twopane mode it sits right on top of each timeline item's role-colored left border (owner blue / reviewer orange / arbiter green / human yellow), masking them. The header's pulse dot already conveys "processing", so suppress the bar in pinned mode and let the role colors show through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Polish room compose: tighter pill input + override global button min-height The 48px button min-height global rule was forcing the compose grid track to 48px, pushing the textarea content visibly down inside the pill. Adding min-height: 0 on the compose button lets the track collapse to ~29px so the placeholder lines up with the send button vertically. Also slimmed paddings (form 3px vert, textarea 4px vert) and added bottom breathing room on the compose section (8px → 14px) at user request. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Format styles.css to multi-line transition values Prettier 3.8.1 prefers multi-line transition shorthand. CI was failing format:check on these single-line transitions in the room-compose rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
439 lines
16 KiB
TypeScript
439 lines
16 KiB
TypeScript
import { Database } from 'bun:sqlite';
|
|
|
|
export function applyBaseSchema(database: Database): void {
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS chats (
|
|
jid TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
last_message_time TEXT,
|
|
channel TEXT,
|
|
is_group INTEGER DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id TEXT,
|
|
chat_jid TEXT,
|
|
sender TEXT,
|
|
sender_name TEXT,
|
|
content TEXT,
|
|
timestamp TEXT,
|
|
seq INTEGER,
|
|
is_from_me INTEGER,
|
|
is_bot_message INTEGER DEFAULT 0,
|
|
message_source_kind TEXT NOT NULL DEFAULT 'human',
|
|
PRIMARY KEY (id, chat_jid),
|
|
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_timestamp ON messages(chat_jid, timestamp DESC);
|
|
CREATE TABLE IF NOT EXISTS message_sequence (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS work_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_folder TEXT NOT NULL,
|
|
chat_jid TEXT NOT NULL,
|
|
agent_type TEXT NOT NULL,
|
|
service_id TEXT,
|
|
delivery_role TEXT,
|
|
status TEXT NOT NULL DEFAULT 'produced',
|
|
start_seq INTEGER,
|
|
end_seq INTEGER,
|
|
result_payload TEXT NOT NULL,
|
|
attachment_payload TEXT,
|
|
delivery_attempts INTEGER NOT NULL DEFAULT 0,
|
|
delivery_message_id TEXT,
|
|
last_error TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
delivered_at TEXT,
|
|
CHECK (status IN ('produced', 'delivery_retry', 'delivered')),
|
|
CHECK (delivery_role IN ('owner', 'reviewer', 'arbiter') OR delivery_role IS NULL)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_work_items_group_agent ON work_items(chat_jid, agent_type, delivery_role, status);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_items_open
|
|
ON work_items(chat_jid, agent_type, IFNULL(delivery_role, ''))
|
|
WHERE status IN ('produced', 'delivery_retry');
|
|
|
|
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
group_folder TEXT NOT NULL,
|
|
chat_jid TEXT NOT NULL,
|
|
agent_type TEXT,
|
|
ci_provider TEXT,
|
|
ci_metadata TEXT,
|
|
max_duration_ms INTEGER,
|
|
status_message_id TEXT,
|
|
status_started_at TEXT,
|
|
prompt TEXT NOT NULL,
|
|
schedule_type TEXT NOT NULL,
|
|
schedule_value TEXT NOT NULL,
|
|
next_run TEXT,
|
|
last_run TEXT,
|
|
last_result TEXT,
|
|
status TEXT DEFAULT 'active',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_next_run ON scheduled_tasks(next_run);
|
|
CREATE INDEX IF NOT EXISTS idx_status ON scheduled_tasks(status);
|
|
|
|
CREATE TABLE IF NOT EXISTS task_run_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
task_id TEXT NOT NULL,
|
|
run_at TEXT NOT NULL,
|
|
duration_ms INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
result TEXT,
|
|
error TEXT,
|
|
FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_task_run_logs ON task_run_logs(task_id, run_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS router_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
group_folder TEXT NOT NULL,
|
|
agent_type TEXT NOT NULL DEFAULT 'claude-code',
|
|
session_id TEXT NOT NULL,
|
|
PRIMARY KEY (group_folder, agent_type)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS paired_projects (
|
|
chat_jid TEXT PRIMARY KEY,
|
|
group_folder TEXT NOT NULL,
|
|
canonical_work_dir TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS paired_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
chat_jid TEXT NOT NULL,
|
|
group_folder TEXT NOT NULL,
|
|
owner_service_id TEXT NOT NULL,
|
|
reviewer_service_id TEXT NOT NULL,
|
|
owner_agent_type TEXT,
|
|
reviewer_agent_type TEXT,
|
|
arbiter_agent_type TEXT,
|
|
title TEXT,
|
|
source_ref TEXT,
|
|
plan_notes TEXT,
|
|
review_requested_at TEXT,
|
|
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
|
owner_failure_count INTEGER NOT NULL DEFAULT 0,
|
|
owner_step_done_streak INTEGER NOT NULL DEFAULT 0,
|
|
finalize_step_done_count INTEGER NOT NULL DEFAULT 0,
|
|
task_done_then_user_reopen_count INTEGER NOT NULL DEFAULT 0,
|
|
empty_step_done_streak INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
arbiter_verdict TEXT,
|
|
arbiter_requested_at TEXT,
|
|
completion_reason TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')),
|
|
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
|
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
|
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status
|
|
ON paired_tasks(chat_jid, status, updated_at);
|
|
CREATE TABLE IF NOT EXISTS paired_workspaces (
|
|
id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
workspace_dir TEXT NOT NULL,
|
|
snapshot_source_dir TEXT,
|
|
snapshot_ref TEXT,
|
|
status TEXT NOT NULL DEFAULT 'ready',
|
|
snapshot_refreshed_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
CHECK (role IN ('owner', 'reviewer')),
|
|
CHECK (status IN ('ready', 'stale'))
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_paired_workspaces_task_role
|
|
ON paired_workspaces(task_id, role);
|
|
CREATE TABLE IF NOT EXISTS paired_turn_outputs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
task_id TEXT NOT NULL,
|
|
turn_number INTEGER NOT NULL,
|
|
role TEXT NOT NULL,
|
|
output_text TEXT NOT NULL,
|
|
verdict TEXT,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(task_id, turn_number, role)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_turn_outputs_task
|
|
ON paired_turn_outputs(task_id, turn_number);
|
|
CREATE TABLE IF NOT EXISTS paired_turn_reservations (
|
|
chat_jid TEXT NOT NULL,
|
|
task_id TEXT NOT NULL,
|
|
task_status TEXT NOT NULL,
|
|
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
|
task_updated_at TEXT NOT NULL,
|
|
turn_id TEXT NOT NULL,
|
|
turn_attempt_id TEXT,
|
|
turn_attempt_no INTEGER,
|
|
turn_role TEXT NOT NULL,
|
|
intent_kind TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
scheduled_run_id TEXT,
|
|
consumed_run_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
consumed_at TEXT,
|
|
PRIMARY KEY (chat_jid, task_id, task_updated_at, intent_kind),
|
|
FOREIGN KEY (turn_id, turn_attempt_no)
|
|
REFERENCES paired_turn_attempts(turn_id, attempt_no)
|
|
ON DELETE CASCADE,
|
|
CHECK (status IN ('pending', 'completed')),
|
|
CHECK (turn_role IN ('owner', 'reviewer', 'arbiter')),
|
|
CHECK (
|
|
intent_kind IN (
|
|
'owner-turn',
|
|
'reviewer-turn',
|
|
'arbiter-turn',
|
|
'owner-follow-up',
|
|
'finalize-owner-turn'
|
|
)
|
|
)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_turn_reservations_task
|
|
ON paired_turn_reservations(task_id, task_updated_at, status);
|
|
CREATE TABLE IF NOT EXISTS paired_turns (
|
|
turn_id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
task_updated_at TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
intent_kind TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
|
CHECK (
|
|
intent_kind IN (
|
|
'owner-turn',
|
|
'reviewer-turn',
|
|
'arbiter-turn',
|
|
'owner-follow-up',
|
|
'finalize-owner-turn'
|
|
)
|
|
)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_turns_task
|
|
ON paired_turns(task_id, task_updated_at, updated_at);
|
|
CREATE TABLE IF NOT EXISTS paired_turn_attempts (
|
|
attempt_id TEXT NOT NULL PRIMARY KEY,
|
|
parent_attempt_id TEXT,
|
|
parent_handoff_id INTEGER,
|
|
continuation_handoff_id INTEGER,
|
|
turn_id TEXT NOT NULL,
|
|
attempt_no INTEGER NOT NULL,
|
|
task_id TEXT NOT NULL,
|
|
task_updated_at TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
intent_kind TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
executor_service_id TEXT,
|
|
executor_agent_type TEXT,
|
|
active_run_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
completed_at TEXT,
|
|
last_error TEXT,
|
|
UNIQUE (turn_id, attempt_no),
|
|
FOREIGN KEY (parent_attempt_id)
|
|
REFERENCES paired_turn_attempts(attempt_id)
|
|
ON DELETE CASCADE,
|
|
FOREIGN KEY (parent_handoff_id)
|
|
REFERENCES service_handoffs(id)
|
|
ON DELETE SET NULL,
|
|
FOREIGN KEY (continuation_handoff_id)
|
|
REFERENCES service_handoffs(id)
|
|
ON DELETE SET NULL,
|
|
FOREIGN KEY (turn_id) REFERENCES paired_turns(turn_id) ON DELETE CASCADE,
|
|
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
|
CHECK (
|
|
intent_kind IN (
|
|
'owner-turn',
|
|
'reviewer-turn',
|
|
'arbiter-turn',
|
|
'owner-follow-up',
|
|
'finalize-owner-turn'
|
|
)
|
|
),
|
|
CHECK (
|
|
state IN (
|
|
'running',
|
|
'delegated',
|
|
'completed',
|
|
'failed',
|
|
'cancelled'
|
|
)
|
|
),
|
|
CHECK (executor_agent_type IN ('claude-code', 'codex') OR executor_agent_type IS NULL)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_turn_attempts_turn
|
|
ON paired_turn_attempts(turn_id, attempt_no);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_turn_attempts_task
|
|
ON paired_turn_attempts(task_id, task_updated_at, attempt_no);
|
|
CREATE TABLE IF NOT EXISTS paired_task_execution_leases (
|
|
task_id TEXT PRIMARY KEY,
|
|
chat_jid TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
turn_id TEXT NOT NULL,
|
|
turn_attempt_id TEXT,
|
|
turn_attempt_no INTEGER,
|
|
intent_kind TEXT NOT NULL,
|
|
claimed_run_id TEXT NOT NULL,
|
|
claimed_service_id TEXT NOT NULL,
|
|
task_status TEXT NOT NULL,
|
|
task_updated_at TEXT NOT NULL,
|
|
claimed_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
FOREIGN KEY (turn_id, turn_attempt_no)
|
|
REFERENCES paired_turn_attempts(turn_id, attempt_no)
|
|
ON DELETE CASCADE,
|
|
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
|
CHECK (
|
|
intent_kind IN (
|
|
'owner-turn',
|
|
'reviewer-turn',
|
|
'arbiter-turn',
|
|
'owner-follow-up',
|
|
'finalize-owner-turn'
|
|
)
|
|
)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at
|
|
ON paired_task_execution_leases(expires_at);
|
|
CREATE TABLE IF NOT EXISTS channel_owner (
|
|
chat_jid TEXT PRIMARY KEY,
|
|
owner_service_id TEXT NOT NULL,
|
|
reviewer_service_id TEXT,
|
|
arbiter_service_id TEXT,
|
|
owner_agent_type TEXT,
|
|
reviewer_agent_type TEXT,
|
|
arbiter_agent_type TEXT,
|
|
activated_at TEXT,
|
|
reason TEXT,
|
|
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
|
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
|
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS room_settings (
|
|
chat_jid TEXT PRIMARY KEY,
|
|
room_mode TEXT NOT NULL,
|
|
mode_source TEXT NOT NULL DEFAULT 'explicit',
|
|
name TEXT,
|
|
folder TEXT,
|
|
trigger_pattern TEXT,
|
|
requires_trigger INTEGER DEFAULT 1,
|
|
is_main INTEGER DEFAULT 0,
|
|
owner_agent_type TEXT,
|
|
work_dir TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL,
|
|
CHECK (room_mode IN ('single', 'tribunal')),
|
|
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS room_role_overrides (
|
|
chat_jid TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
agent_type TEXT NOT NULL,
|
|
agent_config_json TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (chat_jid, role),
|
|
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
|
CHECK (agent_type IN ('claude-code', 'codex'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS service_handoffs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
chat_jid TEXT NOT NULL,
|
|
group_folder TEXT NOT NULL,
|
|
source_service_id TEXT NOT NULL,
|
|
target_service_id TEXT NOT NULL,
|
|
paired_task_id TEXT,
|
|
paired_task_updated_at TEXT,
|
|
turn_id TEXT,
|
|
turn_attempt_id TEXT,
|
|
turn_attempt_no INTEGER,
|
|
turn_intent_kind TEXT,
|
|
turn_role TEXT,
|
|
source_role TEXT,
|
|
source_agent_type TEXT,
|
|
target_role TEXT,
|
|
target_agent_type TEXT NOT NULL,
|
|
prompt TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
start_seq INTEGER,
|
|
end_seq INTEGER,
|
|
reason TEXT,
|
|
intended_role TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
claimed_at TEXT,
|
|
completed_at TEXT,
|
|
last_error TEXT,
|
|
FOREIGN KEY (turn_id, turn_attempt_no)
|
|
REFERENCES paired_turn_attempts(turn_id, attempt_no)
|
|
ON DELETE CASCADE,
|
|
CHECK (status IN ('pending', 'claimed', 'completed', 'failed')),
|
|
CHECK (intended_role IN ('owner', 'reviewer', 'arbiter') OR intended_role IS NULL),
|
|
CHECK (
|
|
turn_intent_kind IN (
|
|
'owner-turn',
|
|
'reviewer-turn',
|
|
'arbiter-turn',
|
|
'owner-follow-up',
|
|
'finalize-owner-turn'
|
|
) OR turn_intent_kind IS NULL
|
|
),
|
|
CHECK (turn_role IN ('owner', 'reviewer', 'arbiter') OR turn_role IS NULL),
|
|
CHECK (source_role IN ('owner', 'reviewer', 'arbiter') OR source_role IS NULL),
|
|
CHECK (target_role IN ('owner', 'reviewer', 'arbiter') OR target_role IS NULL)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_service_handoffs_target
|
|
ON service_handoffs(status, target_role, target_agent_type, created_at);
|
|
CREATE TABLE IF NOT EXISTS memories (
|
|
id INTEGER PRIMARY KEY,
|
|
scope_kind TEXT NOT NULL,
|
|
scope_key TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
keywords_json TEXT NOT NULL DEFAULT '[]',
|
|
memory_kind TEXT,
|
|
source_kind TEXT NOT NULL,
|
|
source_ref TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_used_at TEXT,
|
|
archived_at TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_scope
|
|
ON memories(scope_kind, scope_key);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_active
|
|
ON memories(scope_kind, scope_key, archived_at, created_at);
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
content,
|
|
keywords,
|
|
content='',
|
|
tokenize='unicode61'
|
|
);
|
|
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
INSERT INTO memories_fts(rowid, content, keywords)
|
|
VALUES (new.id, new.content, new.keywords_json);
|
|
END;
|
|
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
INSERT INTO memories_fts(memories_fts, rowid, content, keywords)
|
|
VALUES ('delete', old.id, old.content, old.keywords_json);
|
|
END;
|
|
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
INSERT INTO memories_fts(memories_fts, rowid, content, keywords)
|
|
VALUES ('delete', old.id, old.content, old.keywords_json);
|
|
INSERT INTO memories_fts(rowid, content, keywords)
|
|
VALUES (new.id, new.content, new.keywords_json);
|
|
END;
|
|
`);
|
|
}
|