# Workflow oversized Join artifacts

Workflow Run reducer payloads remain bounded at 4 MiB per activation/effect/event. A
closing parallel or fan-out Join whose canonical JSON aggregate is larger than
that boundary is committed as a `workflow_join_aggregate` artifact instead of
being rejected or copied into every successor.

## Identity and storage

`runartifact.Ref` binds workspace, Run, producer node, producer activation,
artifact kind/version, canonical byte length, and a `sha256:` digest. The
artifact ID is deterministically derived from that immutable binding and is
recomputed during validation rather than trusted as an independent selector.

The `kind` discriminator is closed: `workflow_join_aggregate` identifies a
Join/Fan-out aggregate, `workflow_effect_text` identifies oversized message
text, and `workflow_reducer_value` identifies compacted reducer-state values.
`workflow_run_artifacts` stores only bounded metadata and the cursor secret;
`workflow_run_artifact_blobs` stores the immutable canonical JSON bytes. Both
rows are inserted in the same `SaveTurn` transaction as reducer state,
activation/effect mutations, and events. Duplicate deterministic inserts verify
length and digest and reuse the existing blob. Run deletion cascades both rows.

Small values remain inline. For an aggregate turn whose complete reducer state
would cross TiDB's row-entry ceiling, repeated large branch/input/output values
are compacted to the same typed-reference protocol; the logical 32 MiB reducer
state budget remains unchanged. A value over the 64 MiB artifact budget returns
the stable `run.artifact_budget_exceeded` failure path.

## Materialization and reads

The production Run store resolves a marker only after matching every reference
identity against the authenticated workspace/Run/artifact row and rechecking
stored length, digest, finalized status, and Run lifecycle. The reducer engine
materializes references before pure replay and the projector replaces them
before the next turn is committed. Effect dispatch resolves a content reference
only through the same complete typed reference before writing a message.

`GET /api/v1/workspaces/:workspaceId/workflow/runs/:runId/artifacts/read`
accepts the URL-safe encoded complete `reference`, `section`, repeated `path`,
`cursor`, and byte `limit`. Pages are at most 256 KiB (64 KiB default). Cursor MACs bind the complete reference, canonical JSON-pointer path, requested
limit, digest/version, and byte offset; the client cursor itself carries only
`offset` and `mac`. Wrong owner, Run, artifact, path, cursor, digest, version,
content, deleted/canceled Run, or inaccessible row fails closed.

A non-terminal reducer commit is retryable after a process crash. Replaying the
same graph turn derives the same artifact ID/digest and cannot duplicate
artifacts, activations, effects, or external delivery. A post-commit crash
recovers the exact reference from the Run snapshot. Retention is lifecycle-owned:
artifacts on running/waiting Runs keep `expires_at = NULL`; the bounded Run
worker assigns a seven-day retention deadline only after terminal
success/failure/cancellation and live activations/effects are drained. Deleted
Runs remove artifacts by FK.
The Workflow Run worker runs one bounded, replay-safe maintenance pass that
assigns retention for legacy terminal rows, sweeps only expired terminal rows
with no live children, and removes bounded orphan blobs. It uses the same
transactional Run/artifact lock boundary as page reads, so a retained/live
callback cannot be detached. Nested callback dependency markers are located at
the authorized envelope path and the remaining path is resolved inside the
artifact; scalar inline dependency values retain their existing resolver.
