Kanban backend
Composition
The entry point creates the runtime, then calls
createKanbanService(ctx, runtime) from src/backend/service.ts.
That identical KanbanService instance is passed to agent registration,
registerKanbanApi(ctx, runtime, service, agent) and
registerKanbanLifecycle(ctx, runtime, service, agent).
API registration binds the agent participant synchronizer before requests are
served. Lifecycle registration uses the same binding. Membership changes from
HTTP and tools therefore both await syncBoardParticipants after commit.
A synchronization failure propagates: the membership is already saved, but
conversation access must not be reported as synchronized. No detached promise,
polling or timeout substitutes for that acknowledgement.
All complete routes come from kanbanRoutes; handlers parse their path, query
and body contracts, and validate success responses. Human and agent transports
use the same access owner and mutation implementation. HTTP identities come
only from verified request authentication. Fixed error message IDs correspond
to the plugin's browser strings; SQL and untrusted exception text stay out of
responses.
Transactions and order
Reads and writes acquire the board row lock before access admission. Card and
child IDs must belong to that board. Access reads use the transaction-bound
tx.host.usersRead, never a captured root host inside a transaction.
Project links and handoff reservations use the core's active-Project and
account-access seams. Relinking a board does not retarget an existing card
conversation.
Mutation bodies are validated again by the service, including calls from agent tools. Human assignees must be current board members; selecting Chetty does not start paid work and is refused outside handoff. Removing a member clears their assignments in that same board transaction. Switching from whole team to explicit membership retains the current human member set. A whole-team board cannot exclude one individual while remaining whole-team.
Free integer slots update only the moved item. An occupied requested position inserts before its occupant and shifts subsequent positions by one, retaining their order. Bulk moves allocate a 1024-gap block in supplied card-ID order, shifting the existing suffix beyond that block if needed. Columns and checklists use the same insertion rule. Safe-integer exhaustion raises a conflict; no ID-based tie decides where a drop lands. All cards of a bulk mutation are admitted before any change, so a cross-board ID rolls it back.
Every column DTO has required nonnegative integer cardCount, calculated from
all persisted cards in that column, including archived cards. It is independent
of snapshot visibility, search filters and pagination. Creation/update responses,
board snapshots and card details (including agent reads) share that one SQL
projection. Only cardCount === 0 describes an empty column for deletion; an
active-card list alone cannot establish emptiness. This is a read projection,
not a stored counter, and requires no migration.
Board creation accepts a four-name initialColumnNames tuple supplied by the
browser; absent names keep the English defaults for existing name-only callers.
Roles and order remain fixed for those four initial columns. Names are request
data, not a locale heuristic.
The listMemberCandidates read is admitted under the board lock through
KanbanAccess, then projects current human accounts not already in the board.
Any member or administrator may use it, in person or through their agent.
Label update/delete similarly require membership, not board management.
Search uses SQL predicates, an exact matching count and bounded SQL pagination.
Explicit archived: 'only' excludes active rows before both count and page
selection, regardless of includeArchived. Omitting that mode retains the
existing active/all filter.
Card detail adds checklist, comments, attachment metadata and activity; storage
keys never cross the wire. Descriptions remain HTML/plain-text data and do not
authorize rendering untrusted HTML in the host DOM.
Notifications
Each committed board mutation captures the current human recipients and sends
one account-scoped board-changed plugin event per recipient. Membership
removal also invalidates the removed account's former board view. Activity,
assignment notices, mention notices and member invitations originate from the
same backend owner, including mutations made through tools. Notifications run
after the SQL transaction has committed. A post-commit failure rejects the
operation rather than claiming that all effects succeeded; callers must
refresh before retrying a write.
Card alerts pass only the card title as a template parameter and link to the card. Refusal reasons remain stored as existing browser message IDs on the card and are localized by the detail UI; alerts never interpolate those IDs as natural-language text. Native interrupted outcomes supply no reason detail.
The authoritative process registers the due-alert interval. Active, unfinished cards with a current human assignee receive due-tomorrow and overdue notices. Claims include card, deadline, state and recipient. SQL conflict admission allows one sender; a five-minute lease can be reclaimed after interruption. Only successful delivery marks the matching token delivered. Retrying a send-before-mark uses the same host alert key, preserving host deduplication. Date comparisons use PostgreSQL's current calendar date. Archived cards, archived boards and done columns are excluded.
Attachments and removal
Migration 1 owns attachment_cleanup as well as attachment metadata. There is
no child foreign key on cleanup rows: SQL cascades must not erase pending
filesystem work.
An upload first reserves a generated UUID cleanup key durably. Its normal READ COMMITTED transaction locks that key, admits the board/card, writes the file with exclusive creation, inserts metadata and deletes the reservation. This callback is deliberately not SERIALIZABLE and must never acquire automatic callback retries. On failure the remaining reservation becomes deletable. If a commit acknowledgement is uncertain, committed metadata and reservation removal still share one transaction, so recovery cannot remove a committed file.
Card, board and attachment deletion queue storage keys in the deleting
transaction. User removal queues and removes files uploaded by that account,
but retains comments/history with null authors. Periodic cleanup locks queued
keys with SKIP LOCKED, unlinks files and removes completed queue rows.
Missing files are the explicit idempotent success case; other filesystem
failures propagate and retain the durable queue. Authoritative service startup
also reaps upload reservations left by the previous process; ordinary ticks
never reap in-flight uploads. The host owns service/interval lifetime.
The decoded cap is 5 MiB per file, enforced by the request schema, attachment
metadata schema and database size constraint. File names are metadata only; paths use generated
keys under the plugin data directory. Downloads always use attachment
disposition, nosniff and private, no-store. Only PNG, JPEG, GIF, WebP and
PDF receive preview MIME types; all other content is octet-stream. Browser
previews consume fetched bytes rather than embedding executable content.
User removal clears membership, assignee, founder, creator, mention and actor references while preserving authored prose. It synchronizes participants after commit. Project removal unlinks boards idempotently without deleting cards.
Card agent state
reserveHandoff admits only a human with the plugin grant and linked Project
access, then writes one durable token per card before any external paid work.
The backend stores typed previous/started card projections for rollback.
transitionAgent owns binding, start, refusal, settlement and finish transactions;
external integrations never issue parallel card SQL mutations.
Binding records the prepared session and first owner before participant sync or paid work; it does not move or assign the card. Start rechecks live membership, plugin grant and linked Project, sets working status and Chetty assignment and moves an initial-column card to the next ordinary column. Refusal retains the prepared session/owner and restores handoff-owned fields only while that same start remains current; independent edits survive. A removed original column cannot be restored, and a removed assignee cannot regain membership through rollback. Newer settlement/finish transitions win.
Finish requires the acting agent's exact persisted card session and a nonempty summary. It writes an agent comment, moves to review when present, marks done and alerts current member recipients. Native answered outcomes change working/interrupted to waiting; stopped/failed change working to interrupted, with no reason detail. Done is never downgraded. Shared paused input and restart parking are not settled outcomes and do not independently change this projection.
Integration limits
The handoff route, called by a person or by their agent through ElowenApi, delegates to the agent's prepare/bind/participants/start/send sequence. Core's owner-only settlement hook is observational, with no durable admission receipt or missed-event replay. A hard crash may leave card status stale until a later settled conversation turn, or leave a prepared link without a new run. The plugin never automatically resends paid work, guesses an interruption or uses assistant-text presence as admission evidence. The stored conversation is the place to inspect the result and continue manually.
Membership changes still await participant synchronization after commit. Core's native same-turn participant lock seam permits that call from an existing card turn without waiting for its own send lock; foreign callers retain native serialization. The plugin uses no fire-and-forget participant update or parallel access authority.
The host's authenticated plugin API retains its 8 MiB JSON body limit. A 5 MiB file expands to 6,990,508 base64 characters; the bounded filename and MIME metadata leave the complete upload request below that limit. The plugin needs no host limit change or alternate upload transport.