feat: hflow serve — an HTTP API over a workspace, plus corpus-identity fixes - #116
Merged
Conversation
… it total `episode_id` is a hash of the canonical bytes, so anything folded into those bytes decides whether re-ingesting an unchanged recording dedupes. Two things were in there that should not have been, and one that should. `compute_pipeline_version` folded `hflow.__version__`, and the MCAP header's library string carried it too. Every release therefore minted a new `pipeline_version`, a new `episode_id` for byte-identical inputs, and a new version for every step whose function referenced the `hflow` module -- a CLI-only patch invalidated a whole corpus. `hflow.behavior` replaces it with TRANSFORM_BEHAVIOR_VERSION: a constant a maintainer bumps deliberately when the transform would write different bytes for the same input. The message sort keyed on `log_time` alone, and a stable sort then settled ties by the order the transform happened to append them. On a 20s two-camera episode 1350 of 3200 messages share a timestamp; reversing the tie order moved episode_id 8046ef0b563a7c01 -> 3192361fc21d6305. Sorting on (log_time, topic) makes the order a property of the data instead. RESAMPLE_POLICY_VERSION now folds into `compute_pipeline_version` for episodes that HAVE derived channels: the policy decides those samples and no step hash can see it, because the taught idiom reaches `hflow.resample.to_grid` through the module and a module contributes only its name. Also batches the labels/media quarantine gate: it re-synced a bucket mirror and re-scanned every `episodes` parquet file once per episode. Reading every quarantined episode in one pass took a 50-episode batch on a 2000-file catalog from 96.8 ms to 2.0 ms per episode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A client that wants to draw the generated DAGs had two options: parse the rendered Python, or restate the shape and drift from it. `ingest_dag_topology` makes the library the one owner instead -- master chain, per-stage sub-DAG tasks and edges, which task is mapped, which profiles enable each stage -- and `tests/test_runtime_topology.py` reads the rendered source back to prove the description matches every task id and edge the renderer actually writes. `AirflowClient` gains `task_instances()` and a batch-count argument on `ingest()`, so a caller can read one run's live per-task state and trigger a run with an explicit shard count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… its own `hflow serve` serves one data root over JSON: the catalog as a faceted episode listing with server-compiled filters, curation SQL with previews and pinned manifests, ingest runs and their live per-task state, the registered pipeline, and media resolved back to files under the data root. The API is the product surface, not an implementation detail. Every endpoint publishes a typed response schema -- `_contract.py` owns every payload, so 26 endpoints are typed and none returns a bare object -- and the whole surface is described at `/api/openapi.json`. Nothing is reachable only from a browser, so a UI is a client that can be built, replaced, or duplicated without forking this package. It ships no assets; `HFLOW_UI_ASSETS` or a wheel packaging `hflow_server/static/` supplies one, and without either the root serves a page pointing at the API. Named for what it is rather than for one of its clients. `hflow serve` is also deliberately distinct from `hflow up`: `up` brings up the RUNTIME that processes episodes (an Airflow stack in Docker), while `serve` is one process that reads the data root and can trigger a run on a runtime, executing nothing itself. Either is useful without the other, which is why they are not merged -- and folding the API into Airflow's own FastAPI api-server would inherit Airflow's pins for the read plane, the exact coupling `user-venv-init` exists to prevent. Curation state moves to `<data_root>/curation/state.json`: saved queries and pinned manifests belong to the workspace, not to whichever client wrote them. Separate wheel on purpose: pipeline workers install `hflow` into every task venv and should never carry a web server. The `hflow serve` subcommand imports it lazily, so the SDK does not depend on it. Offline by design -- no CDN, no telemetry, no outbound call. The FastAPI docs pages are disabled because they load Swagger from jsdelivr; the schema is served instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 23, 2026
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Serves a workspace over HTTP, and fixes three ways corpus identity was coupled to things that are not the data.
Split out of #87 so the API can be reviewed and merged without waiting on a decision about the frontend. #87 now stacks on top of this and contains only the React app.
hflow serve— the workspace APIA new wheel,
hflow-server, and ahflow servesubcommand. One process over one data root: the catalog as a faceted episode listing with server-compiled filters, curation SQL with previews and pinned manifests, ingest runs and their live per-task state, the registered pipeline, and media resolved back to files under the data root.The API is the product surface, not an implementation detail of a frontend. Every endpoint publishes a typed response schema —
_contract.pyowns every payload, so all 26 endpoints are typed and none returns a bareobject— and the whole surface is described at/api/openapi.json. Nothing is reachable only from a browser, so a UI is a client: it can be built, replaced, or duplicated without forking this package. The wheel ships no assets;HFLOW_UI_ASSETSor a packagedhflow_server/static/supplies one, and with neither,/serves a page pointing at the schema.Separate wheel on purpose: pipeline workers install
hflowinto every task venv and should never carry a web server.hflow serveimports it lazily, so the SDK does not depend on it.serveis deliberately distinct fromup.upbrings up the runtime that processes episodes (an Airflow stack in Docker);servereads the data root and can trigger a run on a runtime, executing nothing itself. Either is useful without the other — you can browse a bucket corpus with no Airflow anywhere, and CI can ingest with no web server. They are not merged because folding the read plane into Airflow's own FastAPI api-server would inherit Airflow's pinned dependency tree, which is the exact couplinguser-venv-initexists to prevent.Offline by design: no CDN, no telemetry, no outbound call. FastAPI's docs pages are disabled because they load Swagger from jsdelivr; the schema is served instead.
Identity fixes
episode_idhashes the canonical bytes, so anything folded into those bytes decides whether re-ingesting an unchanged recording dedupes.compute_pipeline_versionfoldedhflow.__version__, and the MCAP header's library string carried it too, so every release minted a newpipeline_version, a newepisode_idfor byte-identical input, and a new version for every step referencing thehflowmodule. A CLI-only patch invalidated a whole corpus. Replaced byhflow.behavior.TRANSFORM_BEHAVIOR_VERSION, bumped deliberately when the transform would write different bytes.log_timealone; a stable sort then settled ties by the order the transform happened to append. On a 20s two-camera episode 1350 of 3200 messages share a timestamp — reversing the tie order movedepisode_idfrom8046ef0b563a7c01to3192361fc21d6305. Now(log_time, topic): a property of the data, not of the code.RESAMPLE_POLICY_VERSIONwas invisible. It decides derived-channel samples, and no step hash can see it (the taught idiom reacheshflow.resample.to_gridthrough the module, and a module contributes only its name). Now folded intopipeline_version— but only for episodes that have derived channels, so bumping it never churns a corpus that resampled nothing.TRANSFORM_BEHAVIOR_VERSIONgoes 1 → 2 for the ordering change. Pre-v1, nothing published.Also
ingest_dag_topologydescribes the generated DAGs as data, so a client can draw them without parsing rendered Python.tests/test_runtime_topology.pyreads the rendered source back and checks every task id and edge the description claims.episodesparquet file once per episode. Reading every quarantined episode in one pass took a 50-episode batch on a 2000-file catalog from 96.8 ms to 2.0 ms per episode.AirflowClientgainstask_instances()and a batch-count argument oningest().Verification
638 tests pass;
ruff check,ruff format --checkandty checkclean.🤖 Generated with Claude Code