Agent nodes retain a successful fallback route across iterations and recovery. This requires Agent 0.5.5 or later. A committed agent or model change clears the previous route; recovery restores it only for the same configured agent and model.
userspace.dataflow:inspection is a read-only library for trusted owners that
inspect durable flows and external signal waits. Declare it as a library import.
inspection.flow(dataflow_id) returns the flow's ID, actor ID, opaque execution
context, status and decoded metadata. inspection.pending_signal(dataflow_id, signal_id, node_type) returns the pending node and yield IDs for a node type that
implements the signal-wait protocol. A declaration without a committed yield is
absent; a committed result closes the current episode even while the node is
still active. Only the latest UUID-v7 yield episode for each node is inspected,
matching recovery's ordering. Older results cannot close a newer episode.
These operations retain the caller's database authority. They do not authorize the caller, reconstruct an identity, reserve a wait, or execute a workflow. The owning application must verify the returned flow's actor and terminal status before performing an external action. Use the arm's idempotency key for delivery. The signal query filters by flow, node type, active node status and yield record type rather than loading the entire flow's event history.
Errors are native: confirmed absence has kind NOT_FOUND with code
DATAFLOW_FLOW_NOT_FOUND or DATAFLOW_SIGNAL_NOT_FOUND. Invalid stored metadata
or wait state has kind INTERNAL, is not retryable, and uses
DATAFLOW_FLOW_METADATA_INVALID or DATAFLOW_SIGNAL_STATE_INVALID.
Database failures preserve their native kind, retryability and detail fields;
a database-resource NOT_FOUND is not a missing-flow proof. Legacy empty
metadata encodings remain empty metadata. Existing repository/client methods
retain their behavior.
Nodes that start external work and then wait for a signal use the declarative park contract:
local result, err = n:park({
wait_for_signal = true,
signal_id = stable_signal_id,
arm = {
ref = "component.namespace:arm_function",
args = { correlation_id = stable_signal_id },
},
})park persists the node yield before the orchestrator invokes arm.ref. The arm runs
in an isolated function process under the workflow's recovered actor and scope; arguments
are snapshotted as bounded plain data (scalars and tables with string or positive-integer
keys) before the durable commit. Cycles, runtime values, excessive nesting, and oversized
payloads fail with PARK_ARM_INVALID; metatables and later caller mutations cannot cross
the boundary. Arguments cannot select authority. Arm functions must be idempotent: after a crash
between external success and acknowledgement, restart recovery replays the persisted
declaration when the node reattaches. Persist stable correlation in the arguments rather
than returning an opaque handle. A failed arm returns the structured PARK_ARM_FAILED
error and abandons the tracked wait so a later signal cannot revive it.
Existing n:yield behavior is unchanged.
Checkpoint options resolve as trait defaults, explicit agent_options.checkpoint,
then node config.checkpoint overrides. Maps merge recursively; lists replace,
including an explicit empty list. enabled = false disables checkpointing.
No checkpoint configuration means no implicit checkpointing; Dataflow retains
its existing threshold rules (zero and negative thresholds disable scheduling).
Checkpoints run before the next model turn, after the preceding tool results
have settled. Bindings run before the configured function fallback, and both
receive the effective options; a strict binding failure never falls back.
Without a function fallback, a non-strict binding failure is recorded as a
skipped checkpoint; an explicit host strict = true still fails the workflow.
The host still owns marker persistence and the summary length cap. Checkpointing
is conversation compaction, not durable long-term recall or a hard spending limit.
Opt-in behavior _control proposals use the shared declarative subset:
agent/model/trait/tool targets, session/public metadata, and memory.compact.
Proposals are persisted with their iteration. Outcomes and delegations settle
before policy application; effects and completion metadata are committed via
the existing node transaction machinery. Recovery applies stored proposals
without rerunning resolved tools. Replay is at-least-once, so external provider
writes and lifecycle hooks must be idempotent using stable host refs.
Proposals apply in order against the updated selection. Explicit model, trait
and tool targets survive an agent switch, including empty tool lists. Recovery
reloads the selected agent before another checkpoint, lifecycle hook or model
step; a failed reload fails the node instead of running the previous agent.
CI also kills and restarts a runtime with a pending behavior proposal, then
checks the replacement agent's first step, its settled tool result and compaction.
memory.compact = true persists a request that bypasses the token threshold,
not an explicit disable or a missing provider. False is not cancellation.
Requested compaction also runs when the final response ends the node, rather
than depending on another model step. If a proposal also changes agent, model
or overlays, compaction resolves the committed target's identity, provider and
options, consistently with recovery. The provider still owns long-term memory
storage, retention, retrieval and authorization. No behavior attachment means
no new automatic policy/evaluator calls or loop thresholds.
The host can enable native cleanup through its wippy/dataflow dependency:
parameters:
- name: userspace.dataflow:retention_enabled
value: true
- name: userspace.dataflow:diagnostic_retention_days
value: "30"
- name: userspace.dataflow:retention_interval_seconds
value: "300"
- name: userspace.dataflow:retention_batch_size
value: "1000"retention_enabled defaults to false: the optional process service does not
start until the host enables it. The other defaults are 30 days, 3600 seconds,
and 500 rows per table per batch. Setting days to 0 also disables deletion.
Days accept integers 0–36500, intervals 60–86400 seconds, and batches 1–1000.
The service uses the host's existing target_db and process_host bindings.
Cleanup starts 30 seconds after service startup and then runs on the configured
interval. Each transaction removes at most one batch from each diagnostic table.
It retries on the next interval after a database or configuration error; logs from
dataflow.retention report the policy, candidate counts, and deleted counts.
Only entire parent/child families whose flows are all terminal and older than the retention window qualify. Active, waiting, paused, recent, unknown-state, and unrooted families are preserved. PostgreSQL serializes eligibility with flow status changes and child insertion for each bounded transaction, using a 2-second lock timeout and a 30-second statement timeout; SQLite reserves its writer before reading eligibility. A busy database fails the batch safely and retries later.
The sweeper prunes applied commit history (except each flow's last commit) and
intermediate cycle.state, cycle.function_result, node.input, node.yield,
node.yield.result, and parallel.progress records. It preserves pending commits,
flow and node summaries, flow inputs/outputs, node results/outputs, observations,
actions, and evidence. Full diagnostic replay of pruned old flows is no longer
available. This is diagnostic retention, not deletion of durable application
records or all workflow history. PostgreSQL autovacuum makes deleted space
reusable; the service does not run disruptive VACUUM FULL operations.
For an administrative preview, call the library
userspace.dataflow.retention:sweeper.run(db, {days=30, batch_size=1000, dry_run=true})
with an authorized database handle. The library is not a public HTTP endpoint.
Hosts with mutable permissions bind their native identity resolver through execution_authority_imports and set refresh_perms: true (default false). Dataflow resolves current authority at activation and resume boundaries and preserves explicit narrowing ceilings. The host binding contract describes capture, resolution and existing frame behavior.
Parallel iterator and Cycle template configurations accept optional
iteration_input_routes in raw node commands:
iteration_input_routes = {
{ node_id = root_template_id, discriminator = "default" },
{ node_id = join_template_id, discriminator = "item", transform = "output" },
}Each node_id names a direct template child of the iterator. Routes seed the
Parallel item or the Cycle context (input, state, last_result, iteration)
once per iteration, including nodes with incoming body edges. Discriminators
participate in the target's normal inputs.required gate. Transforms use the
same expression environment and error handling as data targets, with output
set to the item or context.
When routes are present, they replace implicit root item seeding. When absent, the existing root seeding remains unchanged. Parallel passthrough inputs still reach roots. Graph construction rejects targets outside the body, duplicate targets, body dependency cycles, and body edges returning to the iterator. Routes do not add executable nodes or outgoing body edges. Cycle retains its existing iteration limit and fails if continuation exceeds it.