Actor-model agentic UI

Actor-model agentic UI is DevCentr’s second cross-org harness UX contract (after grounded tokens).

In plain language: agent chat is a graph of communicating nodes on disk — not one drifting scroll that pretends to be one topic forever.

A node is an actor: a bounded conversation context with a type, an address, parent/child spawn links, and a mailbox summary coworkers read without loading its full transcript.

Why linear chat is wrong

Today’s chat models assume one thread drifts until the user starts over. Agent harnesses violate that assumption constantly:

  • A coordinator discussion spawns twelve task nodes across repos

  • A task node’s chat tangents into a CLA permissions digression — that should become a discussion child, not noise in the merge transcript

  • Parallel jobs need a grid view; tangents need a graph view — same node store, different projections

Serializing all of that into one scroll repeats aggregate headers and buries blockers. The fix is not "better summaries" alone — it is a persisted node graph the UI renders in parallel.

Node model

Field Meaning

id

Stable node identifier (also actor address suffix)

type

discussion, task, coordinator, … — determines UI chrome and spawn rules

address

Disk path root for this node ($CHAT_ROOT/nodes/{id}/)

spawnedBy

Parent node id — child always knows where it came from

spawned

Child node ids — parent records every node it forked

related

Edges that are not spawn (waits-on, references, soft dependency)

status + summary

Mailbox — latest state for subscribers

viewEpoch

Which layout snapshot last included this node in the grid (optional; see temporal layout)

awaitingUser

When true, temporal fade must not treat the node as idle garbage

Structs mirror disk (1:1)

In-memory node records are not a shadow cache. meta.json and chat.jsonl are authoritative; runtime structs deserialize from them and serialize back on every mutation. Model invocations receive a slice of that struct (mailbox + recent lines + declared wait subscriptions), not a separate RAM-only transcript.

Spawn on drift

When a node’s chat drifts from its declared topic, the harness spawns a child node:

  1. Parent appends child id to spawned[]; child sets spawnedBy to parent id

  2. Parent and child addresses are written to disk before either accepts new messages

  3. Parent chat gets a one-line routing marker; new lines belong to the child

Primary trigger: model-proposed spawn. The model running in the parent should propose a spawn when the user mixes a continuation of the current subject with new ideas that deserve their own bounded context — a common pattern in long agent sessions.

Secondary triggers: user gesture (Spawn tangent), or policy (non-task utterance in a task node).

Boundary ambiguity

When the model cannot tell whether new lines continue the parent topic or fork it, spawn a disambiguation child instead of guessing:

  • Parent pauses routing until the disambiguation node resolves the boundary

  • Disambiguation links parent to one or more candidate children (discussion, task, …)

  • Resolution is recorded in the orchestrator metathread (refs below) and parent spawned[]

Node types (initial set)

Type Role

coordinator

Root orchestrator; grid owner; continuous top-level voice with summarization; may invoke Mixr before spawning routed task nodes

discussion

Exploratory context; may spawn task, discussion, or disambiguation children

task

Bounded job (repo PR, merge, docs sync); publishes status to grid

disambiguation

Short-lived fork resolver when parent/child boundary is unclear

More types can extend the same struct — the harness treats type as routing hints, not a separate storage silo.

Disk layout (required)

All graph state is persisted — not RAM-only. I/O cost is acceptable; losing spawn lineage is not.

$CHAT_ROOT/
  graph.json                 # index: node ids, types, spawn edges, related edges
  orchestrator/
    meta.jsonl               # metathread: coordinator voice + refs into node logs
    index.json               # optional aggregate: nodeId → line ranges, summaries
  nodes/
    {nodeId}/
      meta.json              # full node struct (addresses, spawnedBy, spawned, status, …)
      chat.jsonl             # authoritative messages for this node only
      summaries/
        latest.json          # mailbox
        v{n}.json            # compaction chain
  views/
    epoch-1.json             # optional layout snapshot (which task nodes, positions)
    epoch-2.json
  thread.jsonl               # optional export/replay merge — not source of truth

Every spawn is two writes minimum (parent meta.json, child meta.json + graph.json edge update) before the child accepts traffic. The harness batches where the filesystem allows; correctness beats micro-latency.

Metathread and audit

Each session has two conversational surfaces:

  • Interactive thread — whichever node the user is typing in (nodes/{id}/chat.jsonl)

  • Orchestrator metathread — cross-node narrative plus line refs into child logs

Ref example (one line in orchestrator/meta.jsonl):

{"kind":"ref","node":"task-bitwarden","line":42,"summary":"CLA blocker cleared"}

Audit = walk metathread refs; do not replay one serialized scroll. Subscribers read mailboxes and peek at referenced lines — not a filtered projection of a mega-log.

Wait graph

ACTOR_WAIT_GRAPH default is warn.

Subagents declare subscriptions ahead of time:

Mode Meaning

warn (maybe)

Mailbox may be stale or partial; subagent may continue independent work

enforce (definite)

Block until terminal status — merges, releases, irreversible actions

Example subscription on a node struct: waitOn: [{ node: "task-cla", mode: "warn" }].

UI projections

One graph; multiple views over the same disk store:

  • Grid — filter type=task, show status dot + summary (replaces the old "inbox" layout)

  • Fork panel — selected node; load nodes/{id}/chat.jsonl

  • Graph strip — spawn lineage: parent → children

  • Coordinator / global thread — continuous top-level chat with summarization; not every subagent line

  • Temporal layout — planned view engine (below); embeddable widget, not a second storage model

Hierarchical coordinator mode and global personal-context mode are different views of the same graph, not different stores. Reset in global mode is a scope filter (archive nodes, trim personal context to a bookmark) — not delete disk.

Temporal layout (planned view engine)

Parallel nodes do not naturally "chapter" together. Automatic epoch breaks by clock alone lie to the model when the user left a question unanswered.

Temporal layout is a reusable widget that accepts:

  • node activity timestamps (lastEventAt, lastTouched)

  • optional token-volume series

  • agent-generated timeline bookmarks (keywords, images) as chat progresses

Behaviors (phased):

  • Fade, not delete — idle nodes dim after inactivity unless awaitingUser: true or terminal status

  • Horizontal scrubber — seek a time window; replay which nodes were visible then

  • Heatmap / token strip — faint overlay on the scrubber; resizable height

  • Bookmark panel — sidebar index; map-style detail as user zooms in

Scopes: a global timeline (all nodes under $CHAT_ROOT) and sub-timelines scoped to one node subtree. The widget mounts in the global shell and inside a node detail panel — same engine, different scope input.

Epoch layout snapshots (views/epoch-N.json) become optional bookmark-freeze artifacts when the coordinator explicitly reframes a batch — not the primary navigation model.

Harness variables

ACTOR_AGENTIC_UI = graph-grid | serialized | off
ACTOR_GRAPH_EPOCHS = on | off
ACTOR_WAIT_GRAPH = warn | enforce | off
ACTOR_NODE_STORE = jsonl-per-node | off

Defaults when ACTOR_AGENTIC_UI=graph-grid: ACTOR_NODE_STORE=jsonl-per-node, ACTOR_WAIT_GRAPH=warn.

Variable Behavior

ACTOR_AGENTIC_UI=graph-grid

Persist node graph; emit grid updates for task nodes; route detail chat to node forks; coordinator summaries in main thread

ACTOR_AGENTIC_UI=serialized

Legacy — all lines in one scroll

ACTOR_AGENTIC_UI=off

Single-node mode

ACTOR_GRAPH_EPOCHS=on

Optional layout snapshots when coordinator explicitly reframes a batch (supplements temporal bookmarks)

ACTOR_WAIT_GRAPH=warn (default)

Emit maybe-stale warnings on sibling mailbox reads; subagent may continue parallel work

ACTOR_WAIT_GRAPH=enforce

Block model calls that depend on non-terminal sibling mailboxes

ACTOR_NODE_STORE=jsonl-per-node

Per-node chat.jsonl authoritative; orchestrator metathread holds refs

Migration: PROJECT_INBOX* names are deprecated aliases for one release cycle; map grid-forksgraph-grid, PROJECT_INBOX_CHAPTERSACTOR_GRAPH_EPOCHS.

Reference implementation

DevCentr ships a reference runtime at dev-centr/harness (see Agent harness).

Phase Ships

v0

Disk layout, spawn API, metathread refs, CLI

v1

Mixr model router, wait-graph (warn default), HTTP desk (grid + fork)

v2

Temporal layout engine — scrubber, fade, heatmap, scoped bookmarks

HCI Nerdz demo remains a static teaching surface; the harness is the live $CHAT_ROOT backend.

Relationship to HCI Nerdz

HCI Nerdz owns the attention surface — grid, graph strip, spawn UX, demo, essay.

DevCentr owns harness routing, disk schema, and $CODE_ROOT/harness.md variables.

What this is not

  • Not a replacement for grounded tokens — provenance on words, actor UI on nodes

  • Not ephemeral subagent RAM — every spawn is recorded on disk

  • Not Cursor Task tool UI — harness-neutral node events any renderer can place