Skip to content

Actor topology (Supervisor, Graph, Workspace, Container)

This page is the canonical map of which actor owns what state, who holds whose reference, and which ops cross which boundaries. It is the page to read before modifying any actor in src/actors/.

The tree

smithers (SmithersSupervisor)
├── smithers.graph     (ConstraintGraph)
├── smithers.github    (GitHubAdapter)
├── smithers.containers(ContainerManager)
├── smithers.workspace (WorkspaceActor)
├── smithers.archdocs  (ArchitectureDocWatcher) — when present
└── (ephemeral, per-issue)
    ├── IssueActor      — one per active issue
    ├── ReviewerAgent   — one per dispatched review
    └── CodingAgent     — Path A only; Path B uses system.agents instead

SmithersSupervisor is the only persistent root. Everything else either lives until disposal alongside it, or is created+disposed per issue/review.

SmithersSupervisor

ConcernWhat it owns
_repoConfigured GitHub repo (persisted in .smithers/smithers-config.json)
_maxConcurrentIssuesConcurrency cap (default 3)
_llmConfigsResolved provider list from inferenceCatalog
_activeIssues: Map<number, ActiveIssueInfo>Currently-running sessions
_history: CompletedIssueOutcome[]Terminal outcomes
_browsers: Map<string, ConnectedBrowser>Subscribed browser sinks for activity-frame routing
_workstreamRequestsInbox of cross-actor requests
_chatConstraintsApproved chat-derived constraints per issue
_ratchetReference to RatchetStore
_archWatcher, _escalationPolicy, _providerAffinityOptional integrations, lazy-initialised
_autoDispatchTimer, _autoDispatchEnabledBackground dispatch loop

onBootstrap (SmithersSupervisor.ts:556-617) wires children, loads persisted config, resolves LLM credentials, and arms timers.

ConstraintGraph

The projection engine (ConstraintGraph.ts:60-97). Stateless beyond the cached issue list:

ts
private _issues: GitHubIssueWithConstraints[] = [];
private _githubMount = '';
private _version = 0;
private _ratchetStore: RatchetStore | null = null;
private _lastFetchError: string | null = null;

Accepts (CONSTRAINT_GRAPH_ACCEPTS, ConstraintGraph.ts:41-58):

$introspect, $prompt,
graph.rebuild, graph.constraints-for, graph.neighborhood,
graph.topology, graph.status, graph.summary, graph.dependers,
graph.readiness, graph.dependency-chain, graph.critical-path,
graph.blocker-report, graph.topology-rail, graph.issue-inspector,
graph.conflicts

Each graph.*-for op computes a typed projection on demand from the cached issues. The cache is refreshed by graph.rebuild which calls GitHubAdapter.github.list-issues.

Projection Principle reminder. Every browser pane, every agent prompt, every sign-off checklist, and every review dispatch is derived from one of these projections. The browser computes nothing; it renders typed projections. (DESIGN.md §1.1.)

GitHubAdapter

Single-purpose: wrap the gh CLI. Accepts github.list-issues, github.get-issue, github.refresh, github.set-repo. Caches with TTL (_ttlMs = 60000, _lastFetch).

Two-pass enrichment in _fetchIssues:

  1. Build a prefix map 'TS-8' → issueNumber from titles.
  2. Walk every issue body, parse **Blocked by:** lines and Blocked by #N / Depends on #N, resolve symbolic refs via the prefix map.

This is what makes the dependency graph richer than just GitHub's "linked issues" relation.

WorkspaceActor

Session-scoped UI state owner (WorkspaceActor.ts:71-104). Multiple browser tabs, agents, and CLI sessions share the same workspace via $stateChanged events.

The DOM is NOT the control plane — this actor is. (DESIGN.md §4.)

Accepts (WorkspaceActor.ts:80-99):

$introspect, $prompt,
workspace.focus, workspace.select-issue, workspace.navigate,
workspace.show-tab, workspace.set-overlay, workspace.graph-mode,
workspace.set-graph-mode, workspace.set-filter, workspace.set-queue,
workspace.toggle-pin, workspace.state, workspace.select-edge,
workspace.fork, workspace.attach, workspace.tree

Owns WorkspaceState (the full UI projection) and a private _version. Every mutation increments the version and emits $stateChanged. workspace.tree returns a filesystem snapshot of the working dir for the diff tab — see buildWorkspaceTreeSnapshot (WorkspaceActor.ts:18-61) which walks src/, tests/, types/ (entry limit 200, no node_modules/.git/dist).

ContainerManager

ContainerManager.ts:81-120. Manages Docker containers running Matrix CodingAgents. Subscribes to mx.discovery.heartbeat for auto-discovery. Prunes offline containers every 15s.

Accepts: container.spawn, container.stop, container.list, container.remove, container.images.

State:

ts
private _containers = new Map<string, ContainerRecord>();
private _heartbeatUnsub: (() => void) | null = null;
private _pruneTimer: ReturnType<typeof setInterval> | null = null;

Each ContainerRecord carries the docker container ID, the NATS root, status, last heartbeat, image, label, and WorkerRecord[].

IssueActor

Per-issue. Owns the runtime fingerprint of one active session — branch name, current round number, sign-off report so far, accumulated activity log. The supervisor's _activeIssues.set(N, info) carries an agentMount field that points at this actor.

ReviewerAgent

Per-review. Single-shot: created when the supervisor enters reviewing, lives just long enough to evaluate one constraint, returns its ReviewResult, and disposes. The barrier collects every reviewer's result before the supervisor proceeds.

ArchitectureDocWatcher

Optional. When present, watches a configured set of architecture-doc files. On change, parses constraints out of them and surfaces them via smithers.arch-constraints. The supervisor folds these into every issue's projection — so an architecture-doc edit propagates into the next agent prompt automatically.

Cross-actor message flow (sign-off example)

mermaid
sequenceDiagram
    participant Browser as SmithersApp
    participant Sup as SmithersSupervisor
    participant Graph as ConstraintGraph
    participant Issue as IssueActor
    participant Reviewer as ReviewerAgent

    Browser->>Sup: smithers.start { issueNumber: 73 }
    Sup->>Graph: graph.constraints-for(73)
    Graph-->>Sup: ConstraintProjection
    Sup->>Issue: addChild(IssueActor, { issueNumber: 73 })
    Sup->>Sup: dispatch agent (Path A or B)
    note over Sup: agent runs, emits code.event, eventually code.done
    Sup->>Sup: enter sign-off phase
    Sup->>Sup: validate sign-off report against projection
    Sup->>Reviewer: addChild(ReviewerAgent, { constraintId, evidence }) × N
    Reviewer-->>Sup: ReviewResult × N
    Sup->>Sup: barrier — all back?
    alt all approved
      Sup->>RatchetStore: write entries
      Sup-->>Browser: smithers.converged
    else any rejected
      Sup->>Sup: build feedback, restart agent
    end

Communication patterns

  • Cross-actor invocation: RequestReply.execute(this._context, mount, op, payload, { timeoutMs }). Used everywhere the supervisor needs a typed reply.
  • Activity routing: Activity frames flow on $activity subjects with addressing built by createSmithersAddress/encodeSmithersAddress (src/addressing.ts). Browsers subscribe by addressKey.
  • State broadcast: $stateChanged events flow from actors that own state (Workspace, Graph). Subscribers re-render rather than re-fetching.

See also

Source: projects/matrix-3/packages/smithers/src/actors/SmithersSupervisor.ts:373-412 (state declarations), :556-617 (bootstrap), projects/matrix-3/packages/smithers/src/actors/ConstraintGraph.ts:41-87, projects/matrix-3/packages/smithers/src/actors/WorkspaceActor.ts:71-111.