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:
-
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 Brainworkflowrow (not name / “latest version”)graph_digest(and optionallycontract_digest)- contract projection needed on the Sirloin hot path:
purpose,inputBindings,outputSchema,assetOutputs - recipe-level
billing_policies(not Brainworkflow.pricing)
-
Brain remains SoT for the full graph body (nodes/edges/config) in the
workflowtable. Character compute workflows use a dedicated purpose (working name:CHARACTER_COMPUTE). Restricted nodes declareallowedPurposeson 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:subworkflowis allowed; pins use existingWorkflowSubworkflowPin.MEDIA_GENERATIONis forbidden for character EXECUTE. Prefer not allowingdata:load_character*until the Brain character store deprecation path is clear. -
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 unaryRunis retired (UNIMPLEMENTED); Sirloin dispatches via Bull and completes on character-completion. Validate remains on the Nest-DI character-runtime gRPC listener. -
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”.
-
Flank: node palette filters types via Brain
GET /workflow-node-type?purpose=(each node’sallowedPurposes; 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_idon 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_recipeJSON; a dedicated reverse index is deferred until scale requires it. - Acquire/lease payload must stop relying on a full embedded graph inside
canonical_recipefor 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=brainrejected 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.