Changelog
All notable changes to oryxflow are recorded here. This file is read by humans and by AI coding agents diagnosing regressions after an upgrade, so the format is load-bearing:
Newest first. One
## [version] - YYYY-MM-DDheading per release; version is calverYY.M.Dmatchingsetup.py/oryxflow.__version__. Unreleased work goes under## [Unreleased].Group bullets under
### Added/### Changed/### Deprecated/### Removed/### Fixed/### Security(Keep a Changelog: https://keepachangelog.com/).Every breaking change is a bullet that STARTS with the literal token
BREAKING:and carries a same-bulletMigration:clause with the old→new fix.Name the actual symbol in backticks (
`Task.persist`,`RunResult.summary()`), never prose. Agents grep this file for the symbol in their traceback.
[Unreleased]
[26.7.12] - 2026-07-12
Added
Automatic code invalidation, on by default (
settings.code_version_auto = True): every task derives its code identity from the AST hash of its own class plus the project-local symbols it transitively references (codehash.task_hashes,'<relpath>::<symbol>'granularity), so a real logic edit (in the task or a helper it calls) reruns the task and everything downstream on the nextrun(), overwriting in place — while editing an unrelated sibling task in the same file reruns nothing (one monolithictasks.pystays cheap). References to other Task classes are dependency wiring, never a code dependency (a pinned upstream’s unbumped edit can’t ripple throughrequires()mentions); unresolvable constructs degrade conservatively to whole-module granularity. No attribute to maintain, and comment/docstring/formatting edits never rerun (AST normalization). Existing caches are grandfathered on first contact (baseline stamped, zero reruns). Setsettings.code_version_auto = Falsefor explicit-only tracking. The functional API is covered automatically (auto is ambient, no per-task surface). Records live in<dirpath>/.oryxflow-code-status.jsonand travel with the data dir.Task.code_version(str or int, defaultNone): a per-task pin that suspends automatic tracking of that task’s own logic — it recomputes only on a deliberate bump (the task and everything downstream), for expensive tasks where a refactor-triggered recompute must be a decision, or logic the hash can’t see. Records are mode-aware (they store both the token and thesource_hashesas of the last materialization), and thecode_versionline itself is stripped by the AST normalization (typing it in / deleting / bumping it is a token change, never a source change), so pinning/unpinning unchanged code never recomputes (“just resumes”), an edit masked during a pinned-unbumped window is caught the moment the pin comes off, and pinning in the same edit as a logic change forces a rerun instead of blessing stale output.Dependency propagation folds output identity (
output_id, fresh per actual materialization, preserved across re-stamps andaccept_code): downstream reruns exactly when an upstream rematerialized — pin toggles and accepts never ripple, and areset()+rerun upstream propagates downstream even across separate builds.Staleness advisory for pinned tasks: code changed without a bump → cached output is reused and the run warns via
StalenessWarning(aUserWarningsubclass, visible withoutenable_logging()), a loguru record, acode_warningevent, andRunResult.warnings. The printed/logged channels dedupe per process on the message — parameterized instances of one family produce identical text, and aWorkflowMultirun is one build per flow over shared upstreams, so per-task dedupe would still flood stdout — re-arming when the condition changes or the affected tasks rerun/are accepted;RunResult.warningslists each distinct message once per run (MultiRunResult.warningsdedupes across flows), and only the event stream records every occurrence.oryxflow.accept_code(task)/accept_code(): acknowledge an output-equivalent code change without rerunning. With a task instance it re-stamps the task and its entire upstream dep tree (post-order), stamping a fresh baseline record for outputs that have none yet (this is what clears theoutput predates current codemtime-guard warning after an upgrade);Workflow.accept_code(task=None)/WorkflowMulti.accept_code(task=None, flow=None)wrap it; called bare they cover every imported task family that resolves with the flow’s parameters (a multi-final pipeline is fully blessed in one call, from a fresh process — no prior run needed), and a list of tasks is accepted everywhere (onWorkflowMultiprefer the flow method — the module-level bulk form doesn’t know the flows’ parameters). Prints a one-line summary of what it re-stamped (or that nothing was accepted). The tree walk is fault-isolated: a task whoserequires()/output()raises is skipped and reported instead of aborting the walk (a brokenrequires()also can’t poison the node’s own blessing). Never touchesoutput_id, so accepting never triggers downstream recomputes.TaskData.keep_versions(defaultFalse): withcode_versionset, outputs live under a readable.../<Task>/v<version>/segment so old versions survive bumps (explicit pins only; auto-tracked tasks overwrite in place).Expensive-recompute guard (
settings.code_version_auto_expensive_s, default 600): an auto-tracked task whose last materialization (recorded asduration_s) took longer is held complete when its code changes and the run warns (StalenessWarning, all channels) with the three exits —reset()to recompute,accept_codeif output-equivalent, or pin withcode_version— so a refactor can’t silently burn a long run.None/0disables the guard.Records carry schema/interpreter tags (
state.RECORD_V,py): a record with a different/missingvor Python minor is treated as unverifiable — complete, then silently re-stamped (grandfather trust level,output_idpreserved) — never a mass rerun after an upgrade.build()mtime-revalidates code hashes at most once per module per build (codehash.freeze()/unfreeze()), keeping the auto-hash overhead on small DAGs low.Event stream
oryxflow.events: every run appendsrun_started/task_ran/task_failed/run_finished/code_warning/code_accepted/task_logevents to.oryxflow/events.jsonl(stable head; earlier months offload toevents-YYYYMM.jsonl, immutable). Plain JSONL —tail/grep/jqwork; writes are async and never fail a run; disable withsettings.events = False. Query viaoryxflow.events.status()(session-start: pending warnings, last run per family, recent failures),events.runs(task_family=, flow=, last=),events.iter_events()— all return data and print nothing;events.print_status()prints the status summary (the session-start orientation call for scripts andpython -c).RunResult.run_id,RunResult.reasons({task_id: 'output missing' | 'code change (auto: <file>::<symbol>)' | 'code change (a -> b)' | 'upstream rerun'}),RunResult.warnings.MultiRunResultgains aggregate.ran/.complete/.failed/.reasons/.warningsacross flows.task_ranevents carry params, code fingerprint, source hashes,autoflag, git SHA/dirty, duration and the rerun reason;WorkflowMultistamps each per-flow build’s events with its flow name.Task-authored
self.logger.*(...)lines are captured astask_logevents during a build (works with logging disabled), so in-run scalars become queryable memory.New settings:
settings.events,settings.eventspath,settings.state_filename.
Changed
settings.db(unused) renamed tosettings.state_filename(the per-data-dir record file name,.oryxflow-code-status.json).
[26.7.11] - 2026-07-11
Changed
Documentation rewrite and PyPI packaging updates; no API changes.
[26.6.6] - 2026-06-06
Changed
BREAKING: package renamed
d6tflow->oryxflow. Migration: replaceimport d6tflowwithimport oryxflow(andfrom d6tflow...->from oryxflow...); the public API names are otherwise unchanged.