Write Path
Status: Accepted Reference epics: INK-826, INK-827, INK-828, INK-829, INK-830 ADRs: ADR-017, ADR-018, ADR-019
How a write — whether the user typed it, the agent produced it, a scheduled task emitted it, or an import pipeline delivered it — becomes a persisted change to the workspace. Every write is expressed as a WorldWrite and crosses the submit boundary. There is no back door.
The invariant
Section titled “The invariant”Every write that modifies workspace-visible content is expressed as a WorldWrite value constructed in the domain. The domain cannot be modified by any other means. See ADR-017 and domain rule 1.
This is a domain invariant, not a convention. It is expressed in the type system: the repositories that persist workspace content accept WorldWrite values, not loose arguments. A caller who wants to modify workspace content cannot skip the boundary because there is no function to call that skips it.
The boundary is agnostic to caller identity. The same invariant governs:
| Caller | Origin carried | Lifecycle default |
|---|---|---|
| Editor (user typing) | Authored | Canonical (or as chosen by the author) |
| Agent tool call | AgentProduced | Candidate (submit-boundary §agent-writes-default-to-candidate) |
| Scheduled autonomous task | AgentProduced | Candidate |
| Import pipeline | Imported | Draft |
| Sandboxed CPython executor tool | Per caller (usually AgentProduced) | Per caller (usually Candidate) |
Origin and lifecycle defaults are explained in systems/world/provenance. The rest of this page walks the runtime path.
Overview
Section titled “Overview”Three framings to carry through:
- One construct, one validator. Every caller — a Tauri command from the editor, an MCP tool call from the sidecar, an import step — ends up at the same
WorldWriteconstruction site and the same validator. Malformed writes cannot be applied because the type system refuses them. - Side effects are derivative of the write, not preconditions on it. The
WriteEffectCoordinatorfires the event log, embedding queue, re-validation flagger, and sync queue after the write commits. Side-effect failures do not roll back the write. - Deviation on conflict, never silent override. When a submission conflicts with existing canonical content, a
DeviationRecordis produced alongside (or instead of) applying the write per domain rule 5. Capability denials are not deviations (domain rule 7).
Stage 1: Caller to WorldWrite
Section titled “Stage 1: Caller to WorldWrite”Callers construct a WorldWrite through a typed adapter that enforces the caller’s identity. Adapters populate the fields the domain requires:
| Field | Source | Notes |
|---|---|---|
origin | Caller identity | Editor adapter populates Authored; MCP adapter populates AgentProduced; import adapter populates Imported. Callers cannot pass arbitrary origin values. |
lifecycle | Call context | Editor writes inherit the author’s chosen lifecycle (usually Canonical); agent writes default to Candidate; import writes land as Draft. |
origin_source_id | Caller | Tool id for agent writes; import-run id for import writes; editor session id for author writes. |
derivation_sources | Declared | Tools that are derivation-emitting declare their source arguments at registration. Adapter pulls the source page ids from the call. |
content | Caller | The actual content payload (page create, block update, tag assignment, etc.). |
capability_intent | Caller | The capability the caller claims to be exercising, checked by the boundary. |
The existing author-typed-into-editor path is one instance of this pattern:
Code: apps/desktop/src-tauri/src/commands/page.rs, crates/application/src/page/save_block_content.rs
The editor’s save_block_content Tauri command resolves the active workspace and constructs a WorldWrite carrying origin: Authored, the LoroDoc blob, the materialized text, and the capability intent. The use case — SaveBlockContentUseCase — hands the WorldWrite to the domain validator and then to the repository. The editor’s write is not special; it just uses a different adapter than the agent’s MCP-bridged tool does.
Editor → LoroDoc → blob
Section titled “Editor → LoroDoc → blob”For editor writes specifically, the content payload originates in a LoroDoc CRDT instance kept in lockstep with the TipTap editor:
LoroSyncPluginintercepts every ProseMirror transaction and applies it to the per-blockLoroDoc.LoroUndoPluginreplaces ProseMirror’s built-in undo with Loro-backed undo so the full operation history survives across sessions.- On save, the React handler exports a full CRDT snapshot (
doc.export({ mode: 'snapshot' })) as the blob anddoc.getText(...)as the materialized text. - The blob is the source of truth; the text is a derived projection used only for search indexing. The blob is always passed through unchanged from the frontend to storage — it is never re-serialized from the text. See the CRDT BLOB passthrough invariant in the data-flow overview.
Agent → MCP → WorldWrite
Section titled “Agent → MCP → WorldWrite”For agent writes, the call originates in the Python sidecar and reaches Rust over MCP. The MCP adapter layer is where the WorldWrite is constructed on the agent’s behalf; see agent-session §stage-3-tool-execution-and-the-submit-boundary for the sidecar-side view. Python-native tools that need to write call a Rust tool over MCP; they never reach storage directly.
Import → batch → WorldWrite per file
Section titled “Import → batch → WorldWrite per file”Import adapts a whole batch of files into per-file WorldWrite values, each carrying origin: Imported and lifecycle: Draft. See data-flow/import for the batch shape.
Stage 2: The submit boundary
Section titled “Stage 2: The submit boundary”The boundary is a single domain-layer validator. It enforces, at minimum:
- Well-formedness. Required fields are present and typed; origin and lifecycle are valid enum values.
- Origin consistency. The caller is allowed to claim this origin. Agent callers cannot claim
Authored; import callers cannot claimAgentProduced. - Lifecycle transitions. The requested lifecycle is a valid transition from the target’s current state, per domain rule 3. The agent cannot silently self-promote content from
CandidatetoCanonical. - Derivation internality. Derivation sources are always workspace entities referenced by workspace identifier. Cross-workspace derivation is refused.
- Capability. The capability the caller claims matches the capability required by the operation; denials produce capability errors, not deviations.
On acceptance, the validator applies the write through the repository in a single SQL transaction. Any derivation links declared in the WorldWrite are inserted in the same transaction (derivation_link rows referencing the newly-written page and its sources).
On conflict with canonical content (the four conflict types named in systems/world/deviation-records), the validator produces a DeviationRecord alongside (or instead of) applying the write per domain rule 5. Every conflict produces a record — no agent self-assessment suppresses creation per ADR-019. The deviation record, the deviation_content_ref rows pointing at the involved content, and any applied content changes are written in the same transaction.
On refusal (malformed write, origin violation, capability denial), no content is modified and the caller receives a typed error. Capability denials do not produce deviation records.
Stage 3: SQLite write
Section titled “Stage 3: SQLite write”Code: crates/infrastructure/sqlite/src/workspace/page/ (and siblings for other content families)
A single SQL transaction writes the content change and any world-model side records:
| Table | Column(s) written | Content |
|---|---|---|
pages | raw_markdown, origin, lifecycle, origin_source_id, origin_at, … | Author/agent/import write updates the page row; new rows carry full provenance |
blocks | content_loro, content, content_type | Editor writes store the LoroDoc blob; agent block writes also store a blob (agent-produced blocks are still Loro-backed) |
derivation_link | all | Inserted when the WorldWrite declares derivation sources |
deviation_record | all | Inserted when the submission conflicts with canonical content |
deviation_content_ref | all | Inserted alongside the deviation record, one row per involved content |
The editor-specific invariant holds: the CRDT blob is written as received; never re-serialized from materialized text. The text is a derived projection used by FTS5 and debug tooling only.
FTS5 index triggers on pages.raw_markdown fire automatically when the column updates. See database-schema §fts5-virtual-tables.
Stage 4: WriteEffectCoordinator side effects
Section titled “Stage 4: WriteEffectCoordinator side effects”Code: apps/desktop/src-tauri/src/side_effects.rs
After the transaction commits, the WriteEffectCoordinator fires a set of idempotent, best-effort side effects. They run in parallel and are fire-and-forget: a side-effect failure is logged but does not roll back the write.
Event log
Section titled “Event log”RecordEventUseCase appends an EventLogEntry for the primary mutation (e.g., EntityType::Block, EventType::Updated) and for any side records produced (e.g., EntityType::DerivationLink on create, EntityType::DeviationRecord on create or status transition). Event-log entries carry device_id and event-source metadata so the event-log UI can filter scheduled-autonomous-task-originated entries as first-class per the PL-B resolution.
Embedding pipeline
Section titled “Embedding pipeline”task.events.try_send(EmbeddingEvent::EmbedPage { page_id })EmbeddingManager receives the event via a bounded channel and schedules the page for re-embedding. Pending events are deduplicated (a HashSet prevents queuing the same page multiple times). When the embedding runs it reads the current raw_markdown, generates a 768-dimensional vector via the ONNX Runtime (snowflake-arctic-embed-m-v2.0), and upserts into page_embeddings.
Re-validation flagging
Section titled “Re-validation flagging”When a write modifies content that has derived descendants (i.e., there are derivation_link rows with this page as source_page_id), the coordinator creates revalidation_flag rows for each derived page per domain rule 6 and the standing-scaled mechanics in systems/world/retroactive-revision. The prominence field is snapshotted at flag-creation time from the source’s current standing. The world model does not auto-rewrite derived content; flags are processed by the author (or by a scheduled autonomous task) from the re-validation queue.
Flag creation runs inside the same transaction as the write in the common case, so a crash between write and flag is not observable; if it did occur, the coordinator’s idempotent retry would re-create the flag.
Sync queue
Section titled “Sync queue”The coordinator enqueues a sync entry via SyncQueueRepository::enqueue(). The sync engine drains this queue in background cycles and pushes updates to Supabase when cloud sync is configured. Sync applies uniformly to content tables and world-model primitive tables (derivation links, deviation records, re-validation flags all sync).
Caller paths
Section titled “Caller paths”The same four-stage flow is instantiated by several caller paths. The differences are concentrated in Stage 1 (adapter).
Editor write
Section titled “Editor write”- User types;
LoroSyncPluginupdates theLoroDoc. - User saves; React exports blob and text.
invoke("save_block_content", { slug, blob, text }).- Tauri command validates at the system boundary (non-empty blob, valid slug format) and resolves the workspace and permission guard.
- Editor adapter constructs a
WorldWritewithorigin: Authored, the author’s chosen lifecycle, and the content payload. - Stages 2–4 run as above.
Agent write
Section titled “Agent write”- Planner produces a tool call; LangGraph dispatches it via MCP to a Rust tool.
- MCP adapter constructs a
WorldWritewithorigin: AgentProduced,lifecycle: Candidate(per submit-boundary), and derivation sources from the tool’s registration metadata. - Stages 2–4 run as above.
- The tool returns a
ToolResultover MCP carrying the applied write’s identity and any generated deviation record ids.
Scheduled autonomous task write
Section titled “Scheduled autonomous task write”Identical to the agent write path, except the run was initiated by the task-runner rather than by a user prompt. The write carries origin: AgentProduced and a device_id / event-source metadata distinguishable in the event log.
Import write
Section titled “Import write”For each source file, the import adapter constructs a WorldWrite with origin: Imported, lifecycle: Draft, and an origin_source_id tying the write back to the import run. See data-flow/import for the batch flow and the bulk-canonicalize affordance.
Sandboxed CPython executor write
Section titled “Sandboxed CPython executor write”The Wasmtime-sandboxed CPython executor is exposed as an MCP tool — a capability, not a runtime, per ADR-021. Any write it performs goes through the same MCP adapter as any other agent write and receives origin: AgentProduced when invoked by the agent, or the caller’s origin otherwise. The sandbox is not a back door.
Error handling
Section titled “Error handling”| Failure | Behavior |
|---|---|
| Empty blob | Validation error returned before any DB write |
| Origin inconsistent with caller | DomainError; no write; caller receives typed error |
| Invalid lifecycle transition | DomainError; no write |
| Derivation source is not a workspace entity | DomainError; no write |
| Capability denied | Typed capability error; no write; no deviation record per domain rule 7 |
| Conflict with canonical content | DeviationRecord produced; write may or may not apply depending on conflict type; caller receives WorldWriteResult { applied, deviation_ids } |
| SQLite write failure | Internal error; no side effects fire |
| Embedding queue full | try_send drops the event; next save will re-queue |
| Event log failure | Logged and discarded; write is not rolled back |
| Re-validation flag creation failure | Logged; idempotent retry re-creates the flag |
| Sync-queue enqueue failure | Logged; the write is not rolled back; the next coordinator pass re-enqueues |
Related
Section titled “Related”- systems/world/submit-boundary — the domain invariant this flow implements
- systems/world/provenance — origin and lifecycle semantics
- systems/world/derivation-links — what a derivation link is and when one is created
- systems/world/deviation-records — what a deviation record carries and the four conflict types
- systems/world/retroactive-revision — how re-validation flags are created and triaged
- architecture/domain-rules — every rule enforced by the boundary
- architecture/database-schema — tables this flow writes to
- architecture/data-flow/agent-session — sidecar-side view of a boundary-crossing tool call
- architecture/data-flow/import — batch write flow with
lifecycle: Draft - Sync System — the queue populated at Stage 4
- Embedding System — re-embedding fired at Stage 4
- Search System — FTS5 index maintained via triggers on
raw_markdown
Was this page helpful?
Thanks for your feedback!