ADR-08: Session / workspace persistence

Status: accepted (2026-05-28) Blocks: workstream E (bridge).

Context

nirs4all.session() is public API (frozen since 0.9.0 per nirs4all/CLAUDE.md). It returns a context manager that owns a workspace, lets the user run multiple pipelines against it, and shares cached artifacts / folds / dataset materializations across runs. dag-ml itself is single-run; the bridge must reproduce session semantics or break the API.

Roadmap v1 considered deprecation. Codex flagged: it’s used by nirs4all-studio (the webapp) — breaking would cascade.

Decision

Keep the session API. The bridge implements caching in the adapter layer with the following frozen semantics:

  1. Cache keysha256(envelope_fingerprint + pipeline_fingerprint + controller_manifest_fingerprint). Cross-run hits are by full key only; partial matches are not honored.

  2. Concurrency model — single writer per workspace, multiple readers. A workspace lock file (workspace/.session.lock, OS-advisory) prevents concurrent writers; readers proceed in parallel. Attempting to open a locked workspace as writer raises WorkspaceBusy.

  3. Persistence format — extends the existing SQLite + Parquet workspace (workspace/store.sqlite, workspace/arrays/, workspace/artifacts/). A new table session_cache holds (cache_key, run_id, created_at) rows; entries point to existing artifact + array files. Per-run isolation is preserved.

  4. Bundle relation — exported .n4a bundles do not depend on session cache state; they self-contain everything needed for replay. Removing the workspace must never invalidate previously exported bundles.

  5. Shutdown semanticswith context-manager exit flushes the cache table, releases the lock, and runs VACUUM. SIGKILL / hard crash leaves a stale lock; nirs4all workspace doctor (new CLI command) detects and clears it after checking the holding PID is gone.

  6. Cache invalidation — modifying the dataset (e.g. set_repetition, convert_to_absorbance) recomputes the envelope fingerprint and therefore the cache key — no stale cross-call hits.

Consequences

  • nirs4all/api/session.py keeps its current signature; internals switch from the legacy runner to the bridge.

  • The webapp’s reliance on the session API survives without changes.

  • The dag-ml runtime stays single-run by design; the cache is a binding-layer feature, not a core feature.

  • nirs4all workspace doctor is a new CLI subcommand; documented in CONTRIBUTING.md.

Risk

  • Cache hits across different dag-ml versions are refused (cache key includes the controller-manifest fingerprint which carries the dag-ml version). Users see slower re-runs after upgrades; that’s the intended trade-off.