Skip to main content

Data model

This document describes the first-pass conceptual model for Mesh. The system should not start as “messages in, messages out.” It should start as a graph of actors, conversations, and events. That graph is perspective-scoped: there is no deployment-global conversational worldview shared by every RuntimeAgent. Mesh also has a second, deliberately separate graph domain for execution: a WorkGraph describes how one RuntimeAgent decomposes and executes a complex objective. The conversation graph answers what does this agent know?; the WorkGraph answers what work remains and what depends on what? Do not collapse the two because both happen to contain nodes and edges. See perspective.md for the normative epistemic-isolation boundary and runtime/work-graph.md for the normative execution-graph contract.

Perspective ownership

A RuntimeAgent owns a perspective: the actors, connector identities, conversations, message events, memory, and derived context through which that agent knows the world. This is a data-model boundary, not merely prompt filtering. A graph query that cannot establish which RuntimeAgent’s perspective it is reading is incomplete by definition. These invariants hold mechanically:
  • graph identity never silently crosses RuntimeAgent perspectives
  • two RuntimeAgents may hold distinct Actor rows for the same real-world person
  • cross-connector identity links exist only inside the perspective that established them
  • memory and conversation history from one perspective are not retrieval sources for another
  • co-residency in one Mesh deployment does not collapse graph identities

How the physical schema expresses it

Ownership is a runtime_agent_id column on connectors, actors, and connector_identities, and it is part of the uniqueness rules rather than a filter applied afterward:
  • connectors is unique on (runtime_agent_id, type, name). Two RuntimeAgents observing the same external workspace under the same connector namespace get two connector rows, and therefore two conversation graphs.
  • actors carries ownership directly, because an Actor is reachable without any connector — message events, participation edges, and subject-attributed memory all point at it — and because an Actor may hold identities on several connectors, which is what cross-connector linking means.
  • connector_identities carries it so that composite foreign keys can require an identity’s Actor and Connector to belong to the same perspective. That is what makes “a linkage learned by Lyra does not bootstrap Daedalus’s identity graph” a constraint violation rather than a rule to remember.
conversations and message_events deliberately carry no ownership column: conversations.connector_id is NOT NULL, so their perspective is total and one join away, and duplicating it would create a second version of the same fact that could disagree with the first. The write path resolves both sides of every edge under one perspective, so an observation cannot join rows from two. An earlier iteration expressed ownership only through the connector’s conversation namespace. That was insufficient rather than merely indirect: the namespace resolves from per-install configuration, two installs could resolve to the same string, and the resulting shared connectors row silently merged both agents’ conversations, identities, and actors. Perspective ownership must not depend on configuration values happening not to collide. The conversation namespace still exists and still prefixes conversation keys. It is a legible per-install key, not the isolation boundary.

Core concepts

Actor

A participant as known within one RuntimeAgent’s perspective. Mesh deliberately does not classify what an Actor “really is.” A participant may be a human, an agent, a service, or something else; that ontology is not part of the conversation graph. Actor.kind therefore does not belong in the model. An actor is not the same thing as a connector account. A single Actor may have identities on multiple platforms within the owning perspective. Another RuntimeAgent may independently know the same real-world participant as a different Actor. Suggested fields:
  • id
  • runtime_agent_id — the owning perspective, NOT NULL
  • display_name
  • status (active, disabled, pending)
  • created_at
  • updated_at

RuntimeAgent

A configured agent this install hosts. Agents are rows, created at runtime through the UI. None is privileged and none comes from process configuration — see agent-provisioning.md. An install with zero rows here is valid. A RuntimeAgent is a control-plane entity: which credentials it uses, which models it runs on, which connectors it owns, and which perspective belongs to it. It is not a special class of Actor in the conversation graph. When a RuntimeAgent emits through a connector, another perspective observes that connector identity exactly as it would any other participant. The fact that the control plane happens to host the sender is not conversational context. Suggested fields:
  • id
  • slug
  • display_name
  • status
  • model_primary
  • model_fast
  • model_heartbeat
  • created_at
  • updated_at

AgentConnector

One RuntimeAgent’s presence on one connector installation. For Slack that is one app installed in one workspace, keyed by (api_app_id, team_id). An agent may have many of these — the same agent in several workspaces — and each owns its own credentials. AgentConnector is control-plane configuration. It is also the root from which Mesh can establish which RuntimeAgent perspective an inbound observation belongs to. It must never become a backdoor for telling one agent that another observed Actor is also a RuntimeAgent. Suggested fields:
  • id
  • agent_id
  • connector_type
  • external_app_id
  • external_team_id
  • status
  • created_at

Secret

An encrypted credential, owned by the thing that uses it. Credentials are owned by an AgentConnector, not by the process. They are encrypted with AES-GCM under a master key supplied from outside the database, since a key stored next to its own ciphertext protects nothing. Suggested fields:
  • id
  • owner_type
  • owner_id
  • name
  • ciphertext
  • created_at

Connector

An integration surface within a perspective, such as Slack or Telegram. A connector owns the protocol details for ingress and egress. It belongs to exactly one RuntimeAgent, and its (runtime_agent_id, type, name) triple is unique. Two co-resident agents observing the same external workspace — even under the same namespace — hold two connector rows and do not share one conversational graph. Suggested fields:
  • id
  • runtime_agent_id — the owning perspective, NOT NULL
  • type (slack, telegram, github, etc.)
  • name — the conversation namespace; legible, not the isolation key
  • status
  • created_at

ConnectorIdentity

A connector-specific identity for an Actor inside one perspective. This is what lets that perspective know that a Slack user, a Telegram user, and a GitHub handle may all refer to the same Actor. The mapping is explicit and perspective-scoped. A mapping learned by Lyra does not bootstrap Daedalus’s graph. How a cross-connector mapping is established — the out-of-band possession challenge, its provenance columns, and the abuse-resistance rules — is specified in runtime/identity-linking.md. Suggested fields:
  • id
  • runtime_agent_id — the owning perspective, NOT NULL
  • actor_id
  • connector_id
  • external_id
  • external_handle
  • metadata_json
  • created_at
Composite foreign keys require (runtime_agent_id, actor_id) and (runtime_agent_id, connector_id) to resolve inside one perspective, so a link between one agent’s Actor and another agent’s Connector is not storable.

Conversation

A logical conversation boundary as observed by one RuntimeAgent. This may map to a Slack channel thread, a Telegram chat, a GitHub discussion, or a synthetic Mesh thread. The same external Slack thread may legitimately have separate Conversation rows in multiple RuntimeAgent perspectives. That is intentional. They are separate observations of the same external room, not duplicate canonical state that should be normalized together. Suggested fields:
  • id
  • connector_id
  • external_thread_id
  • external_channel_id
  • title
  • status
  • shape (direct, small_group, channel) — derived from the participant set plus connector-native topology. It is the property that lets engagement policy differ between a DM, where everything is addressed to the agent, and a busy channel, where almost nothing is. See runtime/addressing.md.
  • engagement_override (nullable: require_mention, always) — per-conversation operator escape hatch. Null means “use the shape default,” which is the intended state.
  • created_at
  • updated_at

ConversationParticipant

The explicit relationship between a perspective-scoped Conversation and an Actor who has participated in it. This edge is maintained by the canonical message write path; readers do not reconstruct participation by rescanning message_events. Suggested fields:
  • conversation_id
  • actor_id
  • first_seen_at
  • last_seen_at
The pair (conversation_id, actor_id) is unique. Replayed or out-of-order connector deliveries update the observed time range without creating duplicate participants.

MessageEvent

An immutable observation of a message that entered or left one RuntimeAgent’s perspective. The same external connector event may produce one MessageEvent in Lyra’s perspective and another in Daedalus’s. Their bodies and external ids may match, but their derived meaning may differ. For example, "@Lyra check this" may be engage for Lyra and observe for Daedalus. This is intentional perspective semantics, not accidental duplication. Suggested fields:
  • id
  • conversation_id
  • actor_id — must become nullable; it is NOT NULL today. A message must be persistable with a connector identity and no actor row. Dropping an agent into a 10,000-member channel should not materialize 10,000 actors; actors are promoted on first real interaction, not on first sighting. The applied schema does not do this yet: 0001_initial.sql:53 declares actor_id uuid NOT NULL REFERENCES actors(id) ON DELETE RESTRICT, and no later migration alters it. So the sentence above is the target state, and relaxing the constraint is the first step of Track A2’s actor materialization — a migration over the largest table in the system, exactly the cost this field note was written to avoid. See roadmap/risks.md#r25 (repository).
  • connector_identity_id — who said it, at connector scope. Always present for inbound. This is the cheap identity; actor_id is the expensive one.
  • direction (inbound, outbound)
  • external_message_id
  • parent_external_message_id
  • body
  • body_format — how to parse body. It differs by direction and that is the point: inbound Slack messages are mrkdwn (what Slack sent), outbound replies are markdown (Mesh’s canonical form, since rendering happens at the send boundary and the rendered copy is never stored). A reader that ignores this column and assumes one dialect will show one of the two with its markup visible.
  • engagement (observe, engage, ignore) — this perspective’s addressing verdict. observe messages are context and never trigger a turn.
  • engagement_signal — which tier and which signal produced that verdict, so “why didn’t you answer me?” is answerable without a debugger.
  • metadata_json
  • created_at
A future storage optimization may deduplicate raw connector payload bytes beneath these observations. It must not deduplicate the perspective-specific MessageEvent semantics themselves.

Attachment

A file that rode with a MessageEvent: a screenshot pasted into a question, a document dropped into a thread. One row per file per event, in message_attachments, immutable and deleted with the event. An Attachment is metadata about a file, never the file. Mesh stores the connector’s id for it, its name, MIME type, size, and the connector-authenticated address the bytes can be fetched from (Slack’s url_private, usable only with that install’s own token). The bytes are fetched by the turn that answers the message, bounded, shown to the model, and discarded. Mesh keeps no user files. It is its own table rather than a key in message_events.metadata because metadata is the operator’s provenance stamp and is documented as never read back into a prompt — and attachments are: the current turn hands the images to the model, and history lines carry the names so a later turn knows an earlier message had a picture it is not seeing. A fact a prompt depends on gets a schema. Fields:
  • message_event_id
  • external_id — the connector’s file id; (message_event_id, external_id) is unique, so a replayed delivery re-inserts nothing
  • name, mime_type, size_bytes
  • source_url — empty when the connector withheld it (Slack sends id-only stubs for files the app cannot see); recorded and named, never fetched
  • created_at

Turn

A processing unit for a RuntimeAgent response. A turn is the unit the router schedules and the worker executes. It always belongs to the RuntimeAgent whose perspective contains its Conversation. Turn identity derives from (agent_id, conversation_id), not from message arrival, and at most one turn per pair may be in flight. That is a uniqueness constraint on active turns, not a policy — it is the difference between a busy agent and ten concurrent tool loops fighting over one channel until the host dies. See runtime/addressing.md. Suggested fields:
  • id
  • conversation_id
  • trigger_message_event_id — the first trigger. A turn’s full trigger set is plural (see runtime/interrupt-model.md) and lives on edges.
  • target_actor_id
  • agent_id
  • status (queued, running, succeeded, failed)
  • model_connector_id
  • result_message_event_id
  • created_at
  • updated_at

Run

A durable execution of one Turn. The harness owns Run semantics; see runtime/agent-harness.md. For graph engineering, the important addition is that a root Run has an execution shape:
  • loop — one bounded agent loop directly owns the objective
  • graph — a WorkGraph schedules bounded node executions for the objective
A WorkGraph is not a second conversational identity. It is execution state owned by this Run and RuntimeAgent. Suggested additions:
  • execution_shape (loop, graph)
  • optional work_graph_id
  • optional parent_run_id for child worker executions
  • optional work_node_id identifying which graph node a child Run executes
A model-backed WorkNode may execute as a child Run. The child has fresh bounded execution context and may have its own workspace, but it remains inside the parent RuntimeAgent’s perspective and authority. It is not a new RuntimeAgent or Actor.

WorkGraph

The durable execution topology for one graph-mode root Run. A WorkGraph is a DAG in V1. Loops remain inside bounded worker Runs; retries become node attempts; replanning creates a new graph revision rather than mutating or cycling the admitted graph. Suggested fields:
  • id
  • run_id
  • agent_id
  • status (planning, running, blocked, succeeded, failed, cancelled, budget_exhausted)
  • current_revision_id
  • aggregate budget fields
  • created_at
  • updated_at
A root Run has at most one WorkGraph.

WorkGraphRevision

An immutable proposed topology for a WorkGraph. The planner may propose revisions, but the runtime admits them deterministically before any new topology executes. Completed history is never edited in place. Suggested fields:
  • id
  • work_graph_id
  • revision_number
  • proposed_by_run_step_id
  • status (proposed, admitted, rejected, superseded)
  • structured rejection_reasons
  • created_at
Exactly one admitted revision is current at a time.

WorkNode

A stable logical job inside a WorkGraph. Suggested fields:
  • id
  • work_graph_id
  • stable key
  • kind (agent_loop, deterministic, verify, human_gate, join)
  • objective
  • effect_class
  • declared input contract
  • declared output contract
  • node budget
  • status
  • created_at
  • updated_at
agent_loop nodes do not point at other RuntimeAgents by default. They are bounded worker contexts for the owning RuntimeAgent. Persistent peer-agent collaboration still happens through ordinary connector-visible interaction under perspective.md.

WorkEdge

A dependency between WorkNodes in one admitted revision. Suggested fields:
  • revision_id
  • from_node_id
  • to_node_id
  • optional runtime-defined typed condition
  • expected artifact type/schema
Edges are scheduling/data dependencies, not conversational relationships. They do not belong in thread_edges. The runtime admits a revision only if its WorkEdges form a valid DAG and satisfy graph depth, width, budget, authority, and effect-policy constraints.

WorkNodeAttempt

One execution attempt for a WorkNode. Suggested fields:
  • id
  • work_node_id
  • optional child_run_id for agent_loop nodes
  • status (queued, running, succeeded, failed, blocked, cancelled, awaiting_input, awaiting_approval)
  • attempt number
  • effect/commit state
  • failure reason
  • started_at
  • finished_at
Retries create new attempts; they do not create graph cycles.

WorkArtifact

A durable typed hand-off produced by one node and consumed by downstream nodes. Suggested fields:
  • id
  • work_graph_id
  • producer_node_id
  • producer_attempt_id
  • artifact type/schema
  • inline value or external reference
  • provenance metadata
  • created_at
Artifacts, not whole worker transcripts, are the normal data plane between graph nodes. A downstream context may receive exactly the artifacts declared by its incoming edges plus relevant perspective context resolved under runtime/context-resolution.md.

Commitment

A durable promise owned by one RuntimeAgent that outlives the Run that made it. A Run is one turn; a Commitment is an objective. When an agent replies “on it” and the turn reaches succeeded, the promise must not evaporate with the turn. A Commitment is the row that keeps it alive and the thing a reconciler drives to a verified terminal state. See runtime/commitments.md (repository) for the normative liveness contract. Suggested fields:
  • id
  • runtime_agent_id — the owning perspective, NOT NULL; the authority a reconcile Run assumes, independent of lease_owner
  • conversation_id — where it was promised and where escalation returns
  • origin_run_id — the turn that created it
  • objective
  • objective_hash — with origin_run_id, a database-enforced UNIQUE constraint (both columns NOT NULL, immutable after insert) that makes creation idempotent under replay; the step-2 upsert uses this composite as its conflict target. objective_hash is a hash of the normalized objective (trimmed, case-folded), so a replayed turn collides rather than forking a duplicate. Application-level check-then-insert is insufficient under concurrent resume
  • status (open, in_progress, completed, stalled, escalated, cancelled, failed)
  • desired_state — the target terminal state; completed is the only success target and the default, present so a time-boxed or best-effort commitment can declare a different honest terminal
  • deadline_at — nullable wall-clock deadline, distinct from next_reconcile_at (retry cadence); when passed unmet, the reconciler drives to failed/escalated rather than retrying forever
  • optional work_graph_id — set when the plan decomposes; the Commitment (not a single Run) durably owns this WorkGraph so successive reconcile Runs resolve the same graph
  • evidence_contract — the typed proof required before desired_state
  • lease_owner, lease_expires_at — the E1/B2.5 single-flight idiom; who currently drives, not what authority they hold
  • frontier_fingerprint, frontier_changed_at, attempts — progress tracking
  • next_reconcile_at — due-time for the next attempt, backed off for stalled/escalated
  • created_at
  • updated_at
completed is never self-asserted; it is a transition validated against evidence_contract. A Commitment is control-plane execution state owned by one perspective — like the WorkGraph tables, it is not part of the conversation graph and is not a cross-perspective retrieval source. A WorkGraph is otherwise scoped to one graph-mode root Run via run_id. A graph-mode Commitment outlives any single Run, so the durable owner of its WorkGraph is the Commitment: work_graph_id lives on the Commitment, each reconcile Run resolves the same WorkGraph through it, and the WorkGraph’s run_id records the proposing root Run for audit rather than the sole Run allowed to advance it. See runtime/commitments.md (repository). Resolving work_graph_id is not authority to execute it. Perspective ownership (the constraint discipline above) must hold across the continuation edge: Commitment.runtime_agent_id, WorkGraph.agent_id, and the agent of any reconcile Run that advances the graph must be the same RuntimeAgent. This is a composite-FK / transactional invariant, not a runtime check — the same shape that makes a cross-perspective identity link unstorable rather than merely discouraged. A reconcile Run may not load a WorkGraph whose agent_id differs from its Commitment’s owner, so a stale or malicious work_graph_id cannot execute another agent’s graph under the wrong authority. An authorized ownership handoff (Rule 4 of runtime/commitments.md (repository)) atomically updates the Commitment’s and the WorkGraph’s ownership together, or it does not happen.

ThreadEdge

A relationship in one perspective’s conversation graph. Examples:
  • message replies to message
  • actor participates in conversation
  • conversation belongs to connector
  • turn targets actor
Suggested fields:
  • id
  • from_type
  • from_id
  • edge_type
  • to_type
  • to_id
  • metadata_json
  • created_at
ThreadEdge is deliberately not reused for WorkGraph dependencies. Conversation topology and execution topology have different ownership, invariants, and lifecycle semantics.

Identity rules

  1. Identity is RuntimeAgent-perspective-scoped first and connector-scoped within it. (runtime_agent_id, connector_id, external_id) is the identity key; the same external account seen by two RuntimeAgents is two identities and two Actors.
  2. Cross-connector identity is an explicit mapping inside one perspective, not an assumption.
  3. Multiple actors may participate in the same conversation.
  4. The system must preserve who said what, and who it was for, from the owning perspective.
  5. A reply should never lose attribution just because the thread is busy.
  6. RuntimeAgent identity is control-plane data, not an Actor classification and not deployment configuration.
  7. Seeing an identity is not the same as knowing an actor. Connector identities are created on sighting; actors are promoted on interaction.
  8. Co-residency never implies Actor equivalence, shared memory, or shared conversation state.
  9. Self-recognition is privileged; peer-recognition is not.
  10. WorkGraph workers inherit one RuntimeAgent perspective; they do not create new participant identities.

Execution graph rules

  1. A root Run may execute as one loop or one WorkGraph.
  2. V1 WorkGraphs are DAGs; bounded loops live inside agent_loop node executions.
  3. A model may propose graph topology but may not directly schedule unadmitted topology.
  4. Graph revisions are append-only; completed node history is immutable.
  5. WorkEdges carry dependencies and typed artifacts, never hidden conversational identity.
  6. Child worker authority is a subset of root Run authority.
  7. Node budgets compose under the root Run budget; fan-out does not create budget.
  8. Parallel mutable work must be isolated; one join owns reconciliation.
  9. A worker that wants more decomposition requests replanning from the root graph planner rather than recursively spawning invisible work.
  10. An external side effect in any WorkNodeAttempt commits the root Run under the interrupt-model contract.
  11. A promise that outlives its turn is a Commitment, not a Run. Its terminal completed state is validated against declared evidence, and a reconciler — not the promising agent — owns driving it there. See runtime/commitments.md (repository).

Memory rules

Memory should not be one blob. It should be queryable against the owning RuntimeAgent’s graph:
  • actor-centric
  • conversation-centric
  • time-bounded
  • platform-aware
  • perspective-scoped
Memory written by one RuntimeAgent is never an implicit retrieval source for another RuntimeAgent, even when the control plane can correlate their external participants. WorkGraph artifacts are not automatically durable long-term memory. They are execution records. Promotion from a WorkArtifact into remembered state is an explicit memory write with provenance, not a side effect of a worker having produced it.

First database choice

PostgreSQL is the obvious first choice because both early domains are relational with graph edges: the perspective-scoped conversation graph and the run-scoped WorkGraph. If we later need specialized retrieval, graph scheduling infrastructure, or event search, that should be layered on top of the canonical stores, not replace their ownership and auditability contracts.