Skip to content

fix: linear-time depends_on cycle check (three-colour DFS, both runtimes) - #88

Merged
aswhitehouse merged 3 commits into
mainfrom
fix/diamond-dep-memoization
Jul 10, 2026
Merged

aswhitehouse merged 3 commits into
mainfrom
fix/diamond-dep-memoization

Conversation

@sgriffiths

Copy link
Copy Markdown
Contributor

Problem

depends_on cycle detection in both runtimes uses a recursion-stack-only DFS. That's correct, but it re-walks a shared subgraph once per path that reaches it — on a lattice of stacked diamonds that's 2^N walks before the first model call. Measured on the Python runner's algorithm: 0.4s at 20 levels, 97s at 28, ~4.5 days at 40. All spent validating a graph with fewer than 100 tasks.

Fix

Textbook third colour: alongside the recursion stack ("gray"), track fully-explored nodes ("black") and skip them on re-encounter. The check becomes linear in graph size. Semantics are unchanged:

  • diamond DAGs stay legal (existing depends-on/diamond-graph case),
  • true cycles are still caught, including cycles threaded through a lattice (a done node's subgraph was already proven acyclic when first explored),
  • execution still runs direct deps only — transitive deps are declared, not executed, exactly as §7.2 and the diamond case document.

Applied identically to oas_cli/runner.py::_check_cycles and npm/src/runner.ts::checkChainCycles.

Coverage

  • Unit tests: test_deep_lattice_cycle_check_is_linear (40-level lattice — would hang pre-fix) and test_cycle_through_lattice_still_detected (done-set must not mask real cycles).
  • New conformance case depends-on/deep-lattice (2^40 paths): any runtime that regresses blows the harness's 60s adapter timeout. Matrix regenerated — 33/33 Python, 31+2 unsupported npm, both PASS the new case.

Gates

pytest 505 passed / ruff / mypy clean; tsc clean; full conformance suite green on both adapters.

Note: changelog entry sits under [Unreleased] — if this lands after #87, it belongs in a fresh Unreleased section for the next release.

…ntimes)

The recursion-stack-only DFS re-walked shared subgraphs once per path,
so a lattice of stacked diamonds cost 2^N walks (~97s at 28 levels)
before the first model call. Track fully-explored nodes and skip them:
the check is now linear, diamond DAGs stay legal, and true cycles are
still caught. New conformance case depends-on/deep-lattice (2^40 paths)
holds both runtimes to it under the 60s adapter timeout.
@sgriffiths
sgriffiths requested a review from aswhitehouse as a code owner July 5, 2026 23:17
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
open-agent-spec Ready Ready Preview, Comment Jul 9, 2026 11:03pm

Request Review

@aswhitehouse aswhitehouse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the algorithm is correct and this is a real bug worth fixing before 1.6.0. Three-colour DFS is the textbook answer and it's applied identically in both runtimes; the stack-before-done check ordering is right, so true cycles (back edges to the current path) are still caught and completed subgraphs are provably safe to skip. The 40-level lattice unit test and the conformance tripwire are exactly the right way to hold both runtimes to it.

Two inline comments (one test nit, one pre-existing hardening note — neither blocking) plus one sequencing thought:

Merge order with #87: rather than landing 1.6.0 and immediately having an orphaned [Unreleased] fix, I'd land this PR first and roll the changelog entry into the 1.6.0 block — an exponential pre-flight hang is a good fix to ship in the release rather than the one after. You flagged the same thing in the PR body; happy either way as long as the entry ends up in the right section.

Minor observation, no action needed: the deep-lattice conformance case regresses as a 60s adapter timeout (reported 💥 adapter error) rather than a ❌ FAIL. That's an acceptable tripwire — just worth knowing when reading a future matrix.

Comment thread tests/test_runner.py
Comment thread oas_cli/runner.py
@aswhitehouse aswhitehouse mentioned this pull request Jul 9, 2026
Move the lattice back-edge from a4 to b4 in
test_cycle_through_lattice_still_detected. With the edge on a4 the cycle
was found on the first DFS descent before any node entered the done-set,
so the test did not actually exercise the property it names. Looping the
b-side base instead lets the a-branch mark a4 done first, so the cycle is
detected through b4 after a node has been marked done.

Addresses review feedback from @aswhitehouse on #88.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdPprn1xTRiR1rGZf4jUfP
test: exercise done-set through b4 back-edge in lattice cycle test

@aswhitehouse aswhitehouse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@aswhitehouse
aswhitehouse merged commit 808f0c6 into main Jul 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants