todo implementation
Browser display contract
Todo requires browser UI API 32. The host's ElowenUiRuntime.utils.cardTasks
projects the pushed Todo card once, preserving owner, unresolved blockers,
running labels and measured times. The card uses that helper and the host's
cardTasksAddressable guard; the rail passes the host's already-projected
TaskRailData.tasks directly to TaskList. Neither surface reinterprets
glued text or polls the task route. The host normalizes incoming plugin cards at the emit
boundary; the rail payload is internal host data. Missing cards yield no rows and
empty or partially unaddressable lists have no interactive controls.
The projection is linear in the supplied cards and Todo rows and adds no
network request. Missing or incompatible bundles retain the host's fallback.
The Tasks management picker still reads its authorized route on demand.
The unreleased browser bundle requires the host's hooks.useNow member without
changing its API number. TodoCard calls hooks.useNow(1000, enabled) only
for a live card with a running timestamp; the rail subscribes while one of its
rows has a running timestamp. One host heartbeat serves both surfaces, pauses
notifications while the tab is hidden, catches up on visibility and stops after
the last subscriber leaves. The rail evaluates the current time on each render,
including the render requested by a status action. No plugin timer or network
request is added. A host missing the hook fails explicitly instead of starting
a local clock. Registry UI tests install the candidate's actual hook.
The pinned UI kit predates the host's API 32 declarations. Until that pin is
updated, web-src/runtime.ts derives row fields from the kit's structured card
item type and adds measured duration, rather than defining a second display
projection. UI tests use a test-only host projection port, never plugin logic.
Storage contract
The internal lib/taskStore.mjs module owns the SQLite migrations, task rows, dependency graph, metadata and completed-list aging. lib/tasks.mjs creates one TaskStore during registration and uses it for the HTTP routes, task tools and context providers. Construction applies the existing plugin migrations before preparing queries; no store or context provider is created while Todo is disabled. Account and conversation authorization stays at the registration boundary, which supplies the list key to each store operation.
Batch creation, updates, deletion and clearing retain their transaction boundaries. Graph traversal stays inside the store, and the HTTP and tool entry points share its control-character rule. List reads load only that conversation's tasks and edges; aging runs only when a fresh turn is composed. An unreadable metadata column is logged once per store instance and left byte-for-byte intact by unrelated updates; an explicit metadata patch replaces it. This split adds no query, timer or network request.
Rendering and update contracts
Import xmlEscape from elowen-plugin-shared/xml and call xmlEscape(value) when rendering XML text or attributes. It replaces XML 1.0-forbidden C0 characters, escapes markup and both quote kinds, and preserves Unicode, tab, LF, CR and DEL. It acts only at the XML rendering boundary and does not change stored values or network behavior. It is not single-line label validation: Todo still rejects controls in subjects and owners. Todo's turn context and Twilio's Stream URL attribute are real consumers. The runtime and declarations must be present in the containing core artifact.
Todo task diagnostics use errorText from elowen-plugin-shared/errors. errorText(error) retains Error.message and otherwise uses String(error); conversion failures propagate. It formats only an existing failure and does not log, retry or invent a success result. TaskStore owns UPDATABLE_FIELDS for both update admission and TaskUpdate's corrective hint: supplied scalar fields count as an update, while dependency arrays must be non-empty. Unknown tool keys remain ignored and a patch without a supported change is refused before persistence.
Todo's task picker mounts the shared Modal without the retired intent prop; the host owns overlay presentation and focus. In the turn context, at most 10 finished tasks are listed one by one; older finished tasks fold into one counted line, and their rows and IDs remain until the completed-list grace period ends.