Skip to content

Character compute graphs pin by Brain workflow UUID

Context

The Linear ADR Character contracts live in sirloin chose one store for character recipes so Brisket and session hot paths never depend on a cached remote definition. The POC on project/characters-boning implemented that as embedding the full T-Bone graph inside CharacterRecipe.workflows[].graph, with Brain’s character-runtime validating and executing that snapshot under a Sirloin lease/checkpoint control plane.

That embedding is authoritative for a pin, but it behaves like a cache without invalidation: there is no sourced_workflow_id, Flank’s workflow catalog is not the authoring SoT for character compute, and publishing a Brain workflow cannot signal recipe editors to bump. Sirloin also does not interpret graph.nodes at runtime — it only needs workflow id/purpose, inputBindings, outputSchema, assetOutputs, and recipe-level billing_policies. The full node graph is required by Brain at execute time.

Meanwhile Brain already has versioned workflow rows, Bull-backed WorkflowExecution / NodeExecution resume, and WorkflowSubworkflowPin for one-level subworkflows. Character compute should reuse that machinery without making character EXECUTE a MEDIA_GENERATION run (Generation row + media-completion webhook).

Decision

Split recipe pin from compute-graph SoT:

  1. Sirloin remains authority for character sessions, billing, assets, presentation (stages/fields/actions), recipe publication, and capability projection. A character EXECUTE binding stores an immutable pin:

    • brain_workflow_id — UUID of a published Brain workflow row (not name / “latest version”)
    • graph_digest (and optionally contract_digest)
    • contract projection needed on the Sirloin hot path: purpose, inputBindings, outputSchema, assetOutputs
    • recipe-level billing_policies (not Brain workflow.pricing)
  2. Brain remains SoT for the full graph body (nodes/edges/config) in the workflow table. Character compute workflows use a dedicated purpose (working name: CHARACTER_COMPUTE). Restricted nodes declare allowedPurposes on Brain registry metadata (omit = every purpose, including character compute); publish and Flank palette share that SoT (fail-closed only for nodes that opt into a list). flow:subworkflow is allowed; pins use existing WorkflowSubworkflowPin. MEDIA_GENERATION is forbidden for character EXECUTE. Prefer not allowing data:load_character* until the Brain character store deprecation path is clear.

  3. Execution transport: Sirloin starts a run with a short RPC/HTTP call (workflow UUID + digest + session input). Brain enqueues Bull (WorkflowExecution / NodeExecution). Completion returns via a character-session callback/webhook (not /webhooks/brain/media-completion). The sync character-runtime unary Run is retired (UNIMPLEMENTED); Sirloin dispatches via Bull and completes on character-completion. Validate remains on the Nest-DI character-runtime gRPC listener.

  4. Two-step ship: publishing a Brain workflow creates a new UUID and does not change production character funnels. Only a Sirloin recipe publish that repins the new UUID ships compute changes. Editor UX may prompt bump when a newer published version exists under the same workflow name; run never follows “latest”.

  5. Flank: node palette filters types via Brain GET /workflow-node-type?purpose= (each node’s allowedPurposes; omit = all). Brain publish remains the security boundary.

This narrowly reopens the Linear ADR’s claim that Sirloin stores full “graph definitions as data.” Sirloin still stores the contract pin and all character-domain meaning; only the executable node graph body moves to Brain’s existing workflow store under an immutable UUID pin.

Consequences

Easier

  • Author and version character compute graphs like other T-Bone workflows (Flank, Bull board, boot recovery, subworkflow pins).
  • Smaller recipe documents; no de-facto full-graph cache on the session path.
  • Clear bump/rollback: repin previous brain_workflow_id on the recipe.
  • One purpose-gating owner (Brain node registry allowedPurposes) shared with Flank palette filtering.

Harder

  • Recipe publish must fetch/validate contract by workflow UUID and persist the projection + digests (not a TTL cache).
  • Publish-impact / bump UX must find recipes pinning a Brain UUID — with few active recipes, scan published canonical_recipe JSON; a dedicated reverse index is deferred until scale requires it.
  • Acquire/lease payload must stop relying on a full embedded graph inside canonical_recipe for compute (pin ids + digests + session input instead).
  • Requires Law/Pawel acknowledgment as a delta to the Linear single-store ADR.

Rollback

  • Per-recipe: republish previous recipe version (old pins).
  • Per-kind routing switch unchanged.
  • Production remains adapter-gated (adapter=brain rejected in prod until cutover is explicitly enabled).

Alternatives Considered

  • Keep full graph embed (Linear ADR literal / POC). Rejected for authoring/ops: no Brain workflow SoT, no bump signal, duplicate graphs, second execution/resume stack forever.
  • Embed at recipe publish only (sourced_workflow_id + copied graph). Safer vs ADR wording, but still dual copies and a manual promote step; rejected in favor of pin + Brain SoT for compute body.
  • Live pin by workflow name + latest published. Rejected: Brain publish would silently change production funnels; no explicit test gate.
  • Unary sync RPC for the entire graph. Rejected as the sole model for FOXY-468 (timeouts, long jobs, second ops pattern). Bull is the locked default; unary remains acceptable only as a short-lived transition.
  • Character EXECUTE as MEDIA_GENERATION. Rejected: creates Generation rows and media-completion webhooks inappropriate for creation-session steps.