Context
Extending karma to serve as the durable substrate for agent-coord, a new multi-agent coordination product. Instead of standing up a standalone decision store, we reuse karma's single-DB + existing indexer + existing dashboard primitives. Integration contract was answered in docs/features/2026-04-24-karma-integration-answers.md (this repo).
Updated 2026-04-24 after a senior-designer review pass on the agent-coord design. The body below is authoritative; earlier comments on this issue contain the deltas that led to these updates. Key post-review deltas baked in: decision mirror columns renamed for external-system pluggability, agent_presence has an explicit @human row via is_human column, agent-coord layer references renumbered to the 5-layer model.
What I'm asking for — three independently mergeable pieces
1. v10 → v11 schema migration
Five new tables (no collisions with existing schema; none of these concepts exist in karma today per the integration answers §Q2, §Q10):
room — PK id (external ticket id, e.g. Linear LIN-4821 or GitHub owner/repo#N), title, status, created_at, closed_at?
agent_presence — FK room.id, agent_id ("{repo}:{branch}" for Claude sessions; literal "human" for the director — stable across commits AND across session restarts; not keyed on session_uuid), repo (NULL for human), branch (NULL for human), session_uuid (FK sessions.uuid; NULL for human), is_human BOOLEAN NOT NULL DEFAULT false, joined_at, joined_at_commit (NULL for human), last_seen_at_commit (NULL for human), left_at?
message — PK id UUID v7, FK room.id, thread_id?, in_reply_to?, from_agent_id, to_agents JSON, mentions_attempted JSON, type, body (TEXT or JSON for type=decision), confidence, schema_version, created_at
citation — PK, FK message.id, urn, node_kind, resolved_at_commit, retrieved_via; UNIQUE(message_id, urn)
decision — PK id UUID v7, FK room.id, FK message.id question_id, FK message.id answer_id, body, made_by, confidence, valid_from, valid_until?, FK decision.id superseded_by?, status, mirror_state (pending / synced / failed), mirror_attempts, mirror_last_error?, external_system TEXT NOT NULL DEFAULT 'linear' (enum-ish: linear / github / jira / …), external_ref_id? (comment/issue id in external system; NULL until synced), external_ref_url? (cached URL for dashboard rendering)
Written as an idiomatic idempotent migration in api/db/schema.py (append v11 block, bump SCHEMA_VERSION). All IDs are UUID v7.
On INSERT INTO room, also insert the synthetic @human presence row:
INSERT OR IGNORE INTO agent_presence (
agent_id, room_id, repo, branch, session_uuid,
is_human, joined_at
) VALUES (
'human', ?room_id, NULL, NULL, NULL,
true, CURRENT_TIMESTAMP
);
This lets every consumer (dashboard, escalation rules, reconciler) query SELECT * FROM agent_presence WHERE room_id = ? and get a complete roster without special-casing the human. Without this row, every consumer has to UNION ALL SELECT 'human' which is a papercut that compounds.
2. sync_rooms() indexer module
New module under api/db/ parallel to the existing session indexer. Reads append-only JSONL files at ~/.claude/rooms/<ticket>/messages.<agent_id>.jsonl (one file per agent per room — matches karma's one-writer-per-JSONL idiom).
Invocation: primarily hook-triggered via UserPromptSubmit (called from the agent's Claude Code hook); secondarily via the 300s polling timer in api/main.py as a safety net.
Ingest semantics:
- Parse JSONL lines, validate
schema_version, merge across per-agent files sorted by (created_at, id).
- Per message:
INSERT OR IGNORE INTO message (id, ...) keyed on UUID v7 — idempotent replay.
- For
type=decision messages: wrap message INSERT + decision INSERT in a single transaction.
- Track
last_indexed_message_id per room JSONL file for O(tail) resume on restart.
- Enforce citation requirement at ingest (not speak-time): reject
type=answer messages lacking ≥1 stable-URN citation; write a type=status response back into the room.
- Evaluate escalation rules in the same ingest pass (three if-statements: explicit
@human, implicit all-unsure, time-based NOW() - MAX(created_at) > N). No separate worker.
3. /rooms dashboard page in SvelteKit frontend
Per integration answers §Q6: ~700 LOC, ~1–2 days leveraging existing component library (PageHeader, StatsGrid, FilterControls, etc.). Three files:
routes/rooms/+page.svelte — list view, filtering, search
routes/rooms/+page.server.ts — data loader
- Nav link added to
lib/components/Header.svelte
Also: routes/rooms/[id]/+page.svelte for a single-room timeline view (can ship as follow-up if the estimate expands).
External-mirror reconciler (out of scope for this PR; flagged for awareness)
Once decision rows exist, a reconciler (separate module, can ship in a follow-up PR) reads WHERE mirror_state = 'pending', dispatches on external_system, and POSTs a ticket/issue comment with a hidden HTML marker <!-- decision:$id --> for idempotency. MVP backend is linear; github would use the same marker pattern, just a different API endpoint. No schema migration to add new backends.
Why this matters
Multi-repo tickets currently force users to relay prose manually between Claude sessions (15–30 copy-pastes per Airflow-DAG-shaped ticket). agent-coord eliminates the relay via structured, grounded messages. karma is the durable substrate because:
- Single global SQLite already exists — no new infra
- Existing migration idiom accepts append-only additions cleanly
- Existing dashboard gets free rendering for a new entity type
- Session identity (
sessions.uuid) is already stable and reusable
karma SQLite is the authoritative read surface. JSONL is the write-log / durability seam only — consumers (dashboard, reconciler, escalation rules) always read from karma.
Acceptance criteria
Parent tracker
JayantDevkar/git-radio#3
Reference (design, lives in git-radio)
docs/agent-coord/proposal.md:
- §4 (architecture, per-agent JSONL transport, karma as authoritative read surface)
- §5 L1 (participants & routing;
@human row, addressing grammar)
- §5 L2 (message schema with UUID v7)
- §5 L4 (delivery, attention & escalation — fail-open hook contract + escalation rules)
- §5 L5 (closure — atomic message+decision INSERT; external-system reconciler)
- §6 (decision schema with
external_* columns)
- §7 (MVP boundary)
- §8 (full message-by-message walkthrough with real SQL)
docs/agent-coord/system-design.md has Mermaid diagrams (component map, 5-layer stack, message lifecycle, state machines, end-to-end walkthrough).
Branch: claude/review-agent-coordination-docs-7IPKY.
Context
Extending karma to serve as the durable substrate for agent-coord, a new multi-agent coordination product. Instead of standing up a standalone decision store, we reuse karma's single-DB + existing indexer + existing dashboard primitives. Integration contract was answered in
docs/features/2026-04-24-karma-integration-answers.md(this repo).What I'm asking for — three independently mergeable pieces
1. v10 → v11 schema migration
Five new tables (no collisions with existing schema; none of these concepts exist in karma today per the integration answers §Q2, §Q10):
room— PKid(external ticket id, e.g. LinearLIN-4821or GitHubowner/repo#N), title, status, created_at, closed_at?agent_presence— FKroom.id,agent_id("{repo}:{branch}"for Claude sessions; literal"human"for the director — stable across commits AND across session restarts; not keyed on session_uuid), repo (NULL for human), branch (NULL for human), session_uuid (FKsessions.uuid; NULL for human),is_human BOOLEAN NOT NULL DEFAULT false, joined_at,joined_at_commit(NULL for human),last_seen_at_commit(NULL for human), left_at?message— PKid UUID v7, FKroom.id, thread_id?, in_reply_to?, from_agent_id, to_agents JSON, mentions_attempted JSON, type, body (TEXT or JSON fortype=decision), confidence, schema_version, created_atcitation— PK, FKmessage.id, urn, node_kind, resolved_at_commit, retrieved_via; UNIQUE(message_id, urn)decision— PKid UUID v7, FKroom.id, FKmessage.idquestion_id, FKmessage.idanswer_id, body, made_by, confidence, valid_from, valid_until?, FKdecision.idsuperseded_by?, status, mirror_state (pending/synced/failed), mirror_attempts, mirror_last_error?,external_systemTEXT NOT NULL DEFAULT 'linear' (enum-ish:linear/github/jira/ …),external_ref_id?(comment/issue id in external system; NULL until synced),external_ref_url?(cached URL for dashboard rendering)Written as an idiomatic idempotent migration in
api/db/schema.py(append v11 block, bumpSCHEMA_VERSION). All IDs are UUID v7.On
INSERT INTO room, also insert the synthetic@humanpresence row:This lets every consumer (dashboard, escalation rules, reconciler) query
SELECT * FROM agent_presence WHERE room_id = ?and get a complete roster without special-casing the human. Without this row, every consumer has toUNION ALL SELECT 'human'which is a papercut that compounds.2.
sync_rooms()indexer moduleNew module under
api/db/parallel to the existing session indexer. Reads append-only JSONL files at~/.claude/rooms/<ticket>/messages.<agent_id>.jsonl(one file per agent per room — matches karma's one-writer-per-JSONL idiom).Invocation: primarily hook-triggered via
UserPromptSubmit(called from the agent's Claude Code hook); secondarily via the 300s polling timer inapi/main.pyas a safety net.Ingest semantics:
schema_version, merge across per-agent files sorted by(created_at, id).INSERT OR IGNORE INTO message (id, ...)keyed on UUID v7 — idempotent replay.type=decisionmessages: wrapmessageINSERT +decisionINSERT in a single transaction.last_indexed_message_idper room JSONL file for O(tail) resume on restart.type=answermessages lacking ≥1 stable-URN citation; write atype=statusresponse back into the room.@human, implicit all-unsure, time-basedNOW() - MAX(created_at) > N). No separate worker.3.
/roomsdashboard page in SvelteKit frontendPer integration answers §Q6: ~700 LOC, ~1–2 days leveraging existing component library (
PageHeader,StatsGrid,FilterControls, etc.). Three files:routes/rooms/+page.svelte— list view, filtering, searchroutes/rooms/+page.server.ts— data loaderlib/components/Header.svelteAlso:
routes/rooms/[id]/+page.sveltefor a single-room timeline view (can ship as follow-up if the estimate expands).External-mirror reconciler (out of scope for this PR; flagged for awareness)
Once
decisionrows exist, a reconciler (separate module, can ship in a follow-up PR) readsWHERE mirror_state = 'pending', dispatches onexternal_system, and POSTs a ticket/issue comment with a hidden HTML marker<!-- decision:$id -->for idempotency. MVP backend islinear;githubwould use the same marker pattern, just a different API endpoint. No schema migration to add new backends.Why this matters
Multi-repo tickets currently force users to relay prose manually between Claude sessions (15–30 copy-pastes per Airflow-DAG-shaped ticket). agent-coord eliminates the relay via structured, grounded messages. karma is the durable substrate because:
sessions.uuid) is already stable and reusablekarma SQLite is the authoritative read surface. JSONL is the write-log / durability seam only — consumers (dashboard, reconciler, escalation rules) always read from karma.
Acceptance criteria
agent_presencehasis_humancolumn; synthetic@humanrow inserted on room creationdecisionhasexternal_system,external_ref_id,external_ref_urlcolumns (nolinear_comment_id)sync_rooms()ingests a test JSONL file and produces expected rows, idempotent on replaytype=answer/roomsroute renders a room's message timeline within_reply_to+pinscross-linksParent tracker
JayantDevkar/git-radio#3
Reference (design, lives in git-radio)
docs/agent-coord/proposal.md:@humanrow, addressing grammar)external_*columns)docs/agent-coord/system-design.mdhas Mermaid diagrams (component map, 5-layer stack, message lifecycle, state machines, end-to-end walkthrough).Branch:
claude/review-agent-coordination-docs-7IPKY.