Repository navigation
Releases: humansoftware/synaflow
Release list
v0.32.4
v0.32.4 (2026-09-20)
Fix
- fix(execution): loud post-run stream semantics, unified runtime corpus, fail-loud hints (#134)
Follow-ups #128, #129, #131, #132 (behavioral items only).
#128 — post-run stream ergonomics (corrected diagnosis: no scheduler
deadlock; the original repro hung draining executor.outputs after the run):
- SyncQueueIterator tracks why it closed: exhausted branches keep
raising StopIteration, failed branches re-raise their error, and a
branch closed before being fully consumed raises a descriptive
RuntimeError instead of blocking forever on a dead pump - executor cleanup aborts live fan-outs with FanoutStreamClosedError
(new, in core.exceptions) so a late drain of an unconsumed branch
fails loud; AsyncQueueBranch gains the same closed-state semantics
and terminate(); async cleanup terminal-izes tracked branches - wait_for_workers_after_shutdown gains a log grace window (2s default):
the first poll almost always sees mid-teardown workers, and warning
about a 0.1s teardown destroyed trust in the diagnostic - nested_fanout pack step_results corrected to the observable contract
(terminal drained EACH steps publish None)
#129 — unified runtime corpus:
- new tests/execution/test_corpus_runtime.py executes every registered
pack on its own engine and asserts frozen step_results (replaces the
two hand-maintained per-engine pack lists; engine step_results test
files removed; error-materializer registration tests moved here) - exclusions are explicit and symmetry-checked: complex_parallel and
complex_parallel_mixed deadlock BOTH engines by design — their
terminal step consumes sibling fan-out branches sequentially, which
circular-backpressures a bounded lockstep handoff (issue #128)
#131 — fail-loud build validation (non-breaking parts):
- include-cycle detection unified (dag_expansion.validate_no_include_cycle)
- include return-annotation validated by type inspection instead of a
name substring (aliases validate; look-alike names no longer pass);
unresolvable string annotations keep the tolerant fallback - get_safe_type_hints raises when a callable declares annotations that
cannot be resolved — silent {} disabled all DAG validation; only
fully-unannotated callables resolve to {}
#132 — small smells:
- AsyncQueueBranch.put blocks on the queue properly (wait_for-bounded)
instead of a 1ms hot sleep loop - CsvSerializer fills ragged rows with restval=""
- async error dispatch checks callable(err_mat) before building the
context, mirroring sync - both step runners replace the "output" not in locals() trick with an
explicit _NOT_PRODUCED sentinel (identical semantics)
Tests: 832 passing (was 816). Suite grows by the runtime corpus (24
pack/engine cases), close-semantics unit tests for both handoffs, and
the hints contract tests.
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (0d5e0c5)
v0.32.3
v0.32.3 (2026-09-19)
Fix
-
fix(execution): align async engine with sync engine semantics (audit follow-up) (#133)
-
fix(execution): align async engine with sync engine semantics (sync is canonical)
Body: ports the async engine onto the sync engine's observable contract.
- publish() routes on compiled publish_plan.strategy and completion_policy
- output contracts validated at publish (TypeError), like sync
- materializer crashes propagate; mid-stream item failures keep sync prefix semantics (valid prefix + had_error) with lazy out-of-core access preserved
- producer failures delivered in-band to consumers under any on_error policy (no silent truncation under CONTINUE)
- PipelineStopException queued to consumers carries its cause
- execute() awaits cleanup in finally; cleanup() logs instead of silent pass
- terminal lazy streams stored in executor.outputs like sync
- builder compiles fn_kind + async_stream_deps; runtimes consult metadata, not runtime introspection
- builder compiles publish_async_fanout for single-consumer async streams (EACH consumers unroll via queue branches)
- dropped dead _is_stream_output helpers
Tests updated to canonical contract: annotated under-annotated async generators, fixed the async threshold helper that silently disconnected producer from consumer (broken copy of sync twin), mirrored in-band failure handoff test in sync.
- refactor(core): remove dead builder code, collapse duplicated validators, fail loud on cycles
- delete the unreachable 'requires a custom materializer' validation in
_resolve_materializers (the factory identity it checked had already
been resolved away) together with its now-unused _is_builtin_type
helper; a regression test locks in that the default memory factory
handles custom item types - collapse _validate_no_async_handlers/_validate_no_sync_handlers
(~130 near-duplicate lines) into one parameterized
_validate_handler_execution_modes; error messages preserved - replace the vacuous _validate_declared_step_names duplicate-name call
(it passed an empty dag, so only the reserved check could fire) with
an explicit validate_reserved_step_name; duplicate detection stays in
the expanded pass where it works - get_execution_levels now raises a descriptive ValueError when the
remaining nodes form a cycle instead of silently returning a partial
topological order (regression test added) - delete dead _check_scalar_producer_to_iterable_consumer
- drop the import("typing").Union hack; reuse core
is_async_callable in composite materializers (superset: also detects
partials and async callable objects) - add module docstring to composite.py
- test(corpus): register orphaned nested_fanout packs, fix async linear drift, harden corpus hygiene
- nested_fanout corpus packs existed in both engines but were never
registered in _MODULES — dead specification. Both packs are now
complete (frozen json_dag, execution levels, step results) and
registered; the build-time corpus tests now cover the nested
fan-out/fan-in topology. NOTE: executing this topology currently
deadlocks the sync engine (workers block) — tracked separately; the
runtime step-results assertions stay unregistered until that is
fixed (see step_results explicit pack lists) - fix real sync/async drift in the linear corpus pack: the async twin
silently dropped the step-level observer its sync twin declares
(with the frozen observers json entry) — mirrored with an
async-adapted handler - add a corpus symmetry meta-test: pack sets must match across engines
(modulo the documented sync-only threadpool pack) so an orphaned or
engine-missing pack can never happen silently again - test_runner_step_results (both engines): a failing iterator now
propagates instead of being swallowed into a truncated 'passing' list - conftest run_pipeline fixture: document why it is sync-only
(pipelines are engine-specific by contract; parity lives in the
twins + corpus + meta-test) and drop the misleading dead async branch - PipelinePack.expected_dag was never populated and never read: removed
- test_dag_builder_future docstring no longer claims xfail semantics
for passing contract tests
- style: module docstrings, English state.py docs, small type-hint gaps
- add purposeful module docstrings to the modules flagged in the audit
(core build/validation modules, both executors and step runners,
handoff, stats, overrides, materializers) - translate ExecutionState docstrings to English (language consistency)
and make seed() fail loud on unsupported params types instead of
crashing on AttributeError (plain dict now accepted) - type the previously untyped _identity / async_adapter signatures
- add direct unit tests for ExecutionState (seeding, consumer-scoped
keys, inputs_available) — previously covered only transitively; live
under tests/execution/ since the module is engine-agnostic
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (0b4097a)
v0.32.2
v0.32.2 (2026-07-28)
Fix
-
fix(execution): commit context manager resources before yielding to downstream consumers (#126) (#127)
-
fix(execution): commit context manager resources before yielding to downstream consumers (#126)
Summary
Fixes #126 where context-manager resources in EACH-mode steps remained uncommitted while yielded items were being processed by downstream consumers, causing stale/uncommitted reads in multi-step outbox pipelines.
Changes
- Sync & Async step runners: execute
self.fn(**item_args)inside the context manager block and save the result, allowing the context manager block (with ExitStack()) to exit and commit DB transactions beforeyield resultpasses the item downstream. - Corrected exception handling flow when
PipelineStopExceptionor step execution exceptions occur.
Tests
- Added sync & async unit tests verifying that resource context managers commit prior to downstream step execution (
test_cm_resource_exited_before_downstream_consumer_receives_item). - All 798 tests passing in full test suite.
-
refactor: simplify error handling — use continue instead of has_error/exc_to_raise flags
-
refactor: move try outside with ExitStack + add factory error tests
- Restructure try/except to wrap the with ExitStack block, keeping yield
inside try but outside with (commit before yield, errors still handled) - Add tests for CM factory errors in EACH mode with OnError.CONTINUE
(skips failed item) and OnError.STOP (raises PipelineStopException)
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (c6287b0)
v0.32.1
v0.32.1 (2026-07-28)
Fix
-
fix(execution): enter and exit context manager resources per item in EACH mode (#124) (#125)
-
fix(execution): enter and exit context manager resources per item in EACH mode (#124)
Summary
Fixes #124 where context-manager resources in EACH-mode steps were entered once before the first item and exited once after the last item, instead of wrapping each individual item invocation.
Changes
- Sync & Async argument builders: defer context manager resolution when step is in EACH mode
- Sync & Async step runners: wrap item execution with ExitStack / AsyncExitStack inside generate()
- Sync & Async executors: pass deferred_resources map down to step runners
- Documentation: update resources.md with EACH mode per-item resource lifecycle rules
Tests
- Added sync unit tests in test_runner_each_resource_lifecycle.py
- Added async unit tests in test_async_runner_each_resource_lifecycle.py
- Verified test parity gate (test_parity.py) and sync/async parity suites
- refactor(async_engine): use AsyncExitStack.enter_context for sync CMs instead of nested ExitStack
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (a23e491)
Style
- style(docs): format markdown code snippets with ruff (
0059a2b)
v0.32.0
v0.32.0 (2026-07-17)
Feature
-
feat: support direct CLI flags for nullable primitive pipeline params (#123)
-
feat: support direct CLI flags for nullable primitive pipeline params
-
feat: support direct CLI flags for nullable primitive pipeline params
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (49d5bca)
v0.31.2
v0.31.2 (2026-07-11)
Fix
-
fix(sync_handoff): Fix SyncFanout.join() returning None instead of bool (#120) (#121)
-
fix(sync_handoff): Fix SyncFanout.join() returning None instead of bool (#120)
threading.Thread.join(timeout) returns None unconditionally in
Python 3, regardless of whether the thread exited. The previous
SyncFanout.join() used that return value directly, so it always
returned None (falsy). PipelineExecutor.cleanup() checked
if not exited: and always logged:
SyncFanout pump thread did not exit within 1.0s; ...
The pump was exiting correctly, but join() was a false-positive.
This misled the Issue #120 investigation into suspecting a lock
deadlock when no deadlock existed.
Fix: after the raw self._thread.join(timeout), check
self._thread.is_alive() to determine whether the pump exited.
Verified with 200-iteration stress test (pump + abort racing under
contention): 0/200 stuck (pump always exits). Confirmed the
false-positive was entirely a Python stdlib quirk.
Fixes #120.
- fix(sync_handoff): fix three bugs found via Issue #120 tests
Bug fixes + regression tests:
-
join() RuntimeError — ensure_started could expose
_thread before start(). Other threads reading _thread and
calling .join() got RuntimeError. Fix: catch RuntimeError
in join(), return False ("pump not yet exited"). -
_put_terminal infinite loop — with EOF_MARKER + full
queue, the retry loop never checked _stop. If abort was
called while pump was stuck in the terminal push, pump
leaked a daemon thread. Fix: when _stop.is_set(), drop
unread items (same as ExceptionMarker path). -
Async test hangs — parity tests used bounded queues
(maxsize=2) with no consumer → q.put blocked forever.
Fix: use unbounded queues; contract tested is pump-task
completion, not bounded handoff.
Tests added (3 shared sync+async + 1 sync-only):
- normal_exhaustion / external_abort / blocked_source (both engines)
- full_queue_terminal_abort (sync only, no async analogue)
All 360 execution tests pass (0 regressions).
- refactor(sync_handoff): use is_alive() guard instead of try/except RuntimeError
Replace the defensive RuntimeError catch in join() with an
is_alive() pre-check — cleaner, no exception-based flow
control, and semantically precise ("pump never started or
already exited" vs "pump was assigned but start() didn't run yet").
Also fix the root cause in ensure_started(): start the thread
BEFORE assigning to _thread so there is never a window where
another thread sees _thread as non-None but the thread has
not been started. This makes the join() guard a safety net,
not a primary defence.
Add async parity test for the terminal-push-with-abort
scenario (full queue + cancelled pump task) — same Given
condition exists in both engines, even though the mechanism
differs (asyncio.Task.cancel vs _stop.is_set).
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br>
Co-authored-by: mvallebr <mvallebr@proton.me> (34308ec)
v0.31.1
v0.31.1 (2026-07-11)
Documentation
- docs: move AGENTS.md to root, absorb opencode.json conventions, add worktree convention (#117)
Changes
-
Move
docs/AGENTS.md→./AGENTS.mdand expand it from a 10-line
reading index into a 87-line entry point for AI agents.
Now includes: tool-agnostic conventions, standard verification
commands, four agent roles (build / plan / architect / reviewer),
PR description template. -
Delete
opencode.json. Its tool-specific schema and
permission rules were tied to the opencode CLI which we no longer
use; its agent prompts and commands are absorbed into AGENTS.md
in tool-agnostic form. -
Add section "Parallel-Agent Worktrees" to
HACKING.md,
importing the convention from the analisedefiis repo:- one worktree per parallel agent, under
../synaflow-worktrees/ - branch + dir named
codex/<agent>-<task> - cleanup uses
git worktree remove(never--forcewithout
explicit user approval) - includes note on squash-merged branches (
git branch -d
fails after squash, fall back to-Donly after verifying
the merge commit is on main) - renumbers existing §2-§5 to §3-§6
- one worktree per parallel agent, under
Why
The previous docs/AGENTS.md was a thin reading list. With pi
becoming the primary AI agent tooling, the project needed a single
canonical entry point that:
- lives where AI tooling looks (repo root)
- encodes synaflow-specific AI conventions in tool-agnostic form
- documents the workflow conventions that multiple agents need
to coordinate safely (worktree per agent, no--forcecleanup,
squash-merge caveat)
Verified
- 779/779 tests pass (
uv run pytest tests/) uv run ruff format --check .clean (164 files)uv run ruff check .cleanuv run mkdocs build --strictclean
Diff
AGENTS.md | 87 ++++++++
HACKING.md | 45 ++++-
docs/AGENTS.md | 10 --
opencode.json | 54 ----
4 files changed, 128 insertions(+), 68 deletions(-)
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (a9bf03e)
- docs: repair tutorial tabs and stale dag examples (#116)
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (db6e7b3)
- docs(cli): document lifecycle hooks for project-specific CLIs (#115)
The hooks feature shipped in #114 (PreRunContext, PostRunContext,
RunOutcome, SynaflowCli.pre_run / post_run) is implemented and tested
but invisible to anyone reading the docs: the only documented CLI
pattern was the catalog-mode synaflow --catalog console script,
which does not expose hooks.
Add a new subsection between the existing run documentation and the
end-to-end wrap-up that explains:
- The two-callables pattern on SynaflowCli (pre_run, post_run).
- The shape of PreRunContext / PostRunContext / RunOutcome.
- The error semantics: pre_run must return the right params type,
pipeline failure re-raised after post_run, post_run errors chained
via cause. - A short callout distinguishing hooks (boundary effects around the
whole run) from observers (per-step lifecycle inside the run).
No code change. Pure docs addition (+47 lines, 253 -> 300).
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (5752a16)
Fix
The bootstrap parser in _resolve_run_pipeline relied on
argparse.parse_known_args to find the run subcommand and the
positional pipeline name. argparse has a quirk: when two consecutive
positionals are declared with nargs="?" and an unknown flag
appears between them, argparse silently stops consuming the second
positional and shoves everything after the unknown flag into the
leftover list. Concretely:
tokens = ["run", "--no-observers", "P", "--x", "1"]
before fix: subcommand="run" name=None leftover=[...]
after fix: subcommand="run" name="P" (scanner skips --no-observers)
The bootstrap was unable to recognize P as the pipeline name, so
_resolve_pipeline was never called and the main parser saw
--x 1 as unrecognized arguments:
SynaflowCli(catalog=...).main(["run", "--no-observers", "P", "--x", "1"])
before fix: SystemExit(2) "unrecognized arguments: --x 1"
after fix: runs with params.x = 1, observers disabled
The first form is what argparse users (and the PipelineRegistry.from_module
docs) would naturally write. The second form ["run", "P", "--no-observers", "--x", "1"]
worked only by accident.
Fix: replace the argparse-based bootstrap with a manual token scan.
The scan tolerates any flag in any position (boolean or value-taking),
including typed param flags that are only registered on the main parser
once the pipeline has been resolved.
Fixes #118.
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (7370628)
v0.31.0
v0.31.0 (2026-07-11)
Feature
-
feat(cli): typed params and lifecycle hooks (#114)
-
refactor(cli): add fixed-catalog CLI and remove legacy params
-
feat(cli): add synchronous run lifecycle hooks
-
feat(cli): parse typed pipeline parameters
-
docs(cli): document typed params and lifecycle hooks
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (df17d47)
v0.30.1
v0.30.1 (2026-07-11)
Fix
-
fix(lockstep): memoize DFS to fix exponential walk (#113)
-
fix(lockstep): memoize DFS on (node, barrier) to fix exponential walk
DFS enumeration was exponential in diamond depth (2^N paths per fanout).
For master pipeline (296 steps, 15 effective nested diamonds), the work
scaled to minutes locally and exceeded pytest-timeout in CI.
Replace paths_to (full path list per visit, 2^N x N memory) with
barriers_seen (set[bool] per descendant, O(N) memory) and memoize the
walk on (node, barrier_status). barrier is bool, so each (node, barrier)
pair is visited at most twice per fanout walk. Total: O(N x fanouts).
Verified on 296-step DAG with 12 nested diamonds + 100 side branches:
before: 8.5s, 409 MiB peak, >30s timeout.
after: 0.014s, 0.2 MiB peak, 0.017s at 20 diamonds.
Regression test test_validate_lockstep_symmetry_is_linear_in_diamond_depth
asserts <1s on a 15-diamond chain (pre-fix: ~5 minutes).
Fixes the hang reported in analisedefiis PR #149 where pytest-timeout
fired at ~200s during cli.main(['run', 'master', ...]).
- style: add trailing newline to lockstep_validation.py
ruff format --check was failing in CI on a missing trailing newline. The
file was already correct semantically (8/8 tests pass) — just needed
the EOF marker that ruff format enforces.
Re-runs of the format-on-save didn't pick this up because the editor
was saving without explicit newline-at-EOF.
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (1737938)
v0.30.0
v0.30.0 (2026-07-10)
Feature
-
feat(registry): validate pipelines at registration (#112)
-
refactor(registry): compile pipelines at registration
-
test(observers): avoid cross-scope event ordering assumption
-
chore: sync uv lock with v0.29
Co-authored-by: Marcelo Elias Del Valle <marcelo@mvalle.br> (e6a6add)