scheduler: Spawn inherits WithObserve boundaries via GetBoundaries(k) (ADR-DOE-CORE-001)#479
Merged
proboscis merged 2 commits intoJul 5, 2026
Conversation
…phase 1) Repro for issue scheduler-spawn-drops-observer-boundary: observers inside scheduled() never see effects performed by Spawn'd tasks, while handlers at the same position are inherited. 5 tests fail at HEAD (case-A repro, interleave fidelity, Gather, nested Spawn, get_inner_boundaries primitive); the case-B control (observer outside the scheduler) passes and stays as regression guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (ADR-DOE-CORE-001)
Observers placed inside scheduled() never saw effects performed by Spawn'd
tasks: the Spawn branch captured only prompt handlers (get_inner_handlers /
GetHandlers(k)) and the child fiber's parent chain roots at the scheduler,
so spawn-site observer boundaries were silently dropped.
Fix — symmetric, order-preserving boundary inheritance:
- doeff-vm-core: DetachedFiberChain::boundary_callables() walks the detached
chain collecting prompt AND intercept boundaries, innermost-first, tagged
BoundaryKind::{Handler,Observer}.
- doeff-vm bridge: new non-consuming query GetBoundaries(k) (mirrors
GetHandlers(k)) returning ["handler"|"observer", callable] pairs with the
catching handler as last entry.
- doeff.handler_utils.get_inner_boundaries(k): drops the caller entry;
RuntimeError if the chain does not terminate at a handler (fail-fast).
- scheduler: Spawn captures inner_boundaries and pick_next reinstalls each
kind (WithHandler / WithObserve) preserving the interleaved nesting order,
so observers do not over-observe handler-body effects. Boundaries stay
owned by the child task and are released in _release_task_refs.
- semgrep: doeff-scheduler-spawn-must-capture-boundaries bans
get_inner_handlers regressions inside scheduler.py.
- ADR-DOE-CORE-001 (docs/adr, executable) records the design decision.
GetHandlers and its existing consumers (try_handler, doeff-traverse, ...)
are unchanged. Mask boundaries remain non-inherited (out of scope, ADR R5).
Issue: scheduler-spawn-drops-observer-boundary
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 5, 2026
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.
Fixes issue scheduler-spawn-drops-observer-boundary (
/home/kento/repos/doeff-VAULT/issues/scheduler-spawn-drops-observer-boundary.md).Problem
WithObserveplaced insidescheduled()never saw effects performed bySpawn'd tasks, whileWithHandlerat the same position was inherited. Mechanism: the Spawn branch captured only prompt handlers (get_inner_handlers→GetHandlers(k)), and a spawned task's fiber chain roots at the scheduler's evaluation context, so spawn-site observer boundaries were silently dropped (call_all_observerswalks the parent chain and never reaches them).Design decision (issue 論点 1–3) — ADR-DOE-CORE-001
The issue asked for an explicit design judgment. Decision (recorded as executable ADR
docs/adr/defadr_doeff_core_001_spawn_boundary_inheritance.hy):handler(WithObserve(obs, body))nesting, effects performed by that handler's own body are dispatched above the observer and must stay unobserved — a flat "observers outermost" reinstall would over-observe (e.g. duplicate OTel spans). Covered by a dedicated test.GetBoundaries(k)(mirrorsGetHandlers(k)), returning innermost-first["handler"|"observer", callable]pairs with the catching handler as last entry, plus helperdoeff.handler_utils.get_inner_boundaries(k)that drops the caller entry (RuntimeError if the chain doesn't terminate at a handler — fail-fast).GetHandlersand all its existing consumers (try_handler, doeff-traverse, agents) are untouched.WithHandler/WithObservewrappers owned by the child task's program, released by_release_task_refson completion/failure/cancel.GetBoundariesborrows the continuation without consuming it (multi-shot safe). Scheduler machinery (wrap_task'sTaskCompleted) performs outside the reinstalled boundaries, so subtree observers do not observe scheduler internals. Mask boundaries remain non-inherited (out of scope, ADR R5).Changes
packages/doeff-vm-core/src/continuation.rsBoundaryKindenum +DetachedFiberChain::boundary_callables()/Continuation::boundary_callables()(interleaved walk, innermost-first)packages/doeff-vm/src/{do_expr,python_generator_stream,pyvm}.rsGetBoundaries(k)pyclass, eager classify →Pure([[kind, callable], ...]), registrationpackages/doeff-vm/doeff_vm/__init__.py(.pyi)doeff/program.py,doeff/__init__.py_DOEXPR_TYPESdoeff/handler_utils.pyget_inner_boundaries(k)helper (+ docstring note onget_inner_handlers)packages/doeff-core-effects/doeff_core_effects/scheduler.pyinner_boundaries;pick_nextreinstalls each kind via_reinstall_boundarypreserving order;_release_task_refsupdated.semgrep.yamldoeff-scheduler-spawn-must-capture-boundaries— bansget_inner_handlersinsidescheduler.py(regression guard)docs/adr/defadr_doeff_core_001_spawn_boundary_inheritance.hytests/test_spawn_observer_inheritance.pyVerification
The issue predates the
## Verificationtemplate section; mapping below covers every claim in its 症状/再現/論点 sections. No substitutions or weakenings.scheduled(WithObserve(obs, mark_handler(main)))must observechild-effecttests/test_spawn_observer_inheritance.py::test_observer_inside_scheduler_sees_spawned_child_effects+ repro script output belowWithObserve(obs, scheduled(...))keeps working::test_observer_outside_scheduler_still_sees_all::test_spawned_observer_does_not_see_handler_internal_effects(discriminates interleaved vs flat reinstall)GetObservers-equivalent opt-in query primitive (論点 2)::test_get_inner_boundaries_shape(GetBoundaries viaget_inner_boundaries)GetBoundariesis a non-consuming borrow likeGetHandlers; boundaries released in_release_task_refs::test_gather_multiple_children_all_observed,::test_nested_spawn_inherits_observer_transitivelytests/test_spawn_with_handlers.py(9 tests), full suitedocs/adr/defadr_doeff_core_001_spawn_boundary_inheritance.hy(hit fixture = oldget_inner_handlersline in scheduler.py path; clean fixture = same call in handlers.py where it stays legitimate)Issue repro script (verbatim from issue), after fix
(before fix @ cacc4b8, A observed only
['main-effect']— confirmed by the TDD-phase-1 failing run)Test / lint runs
uv run pytest tests/test_spawn_observer_inheritance.py -q→ 6 passed (5 of them failed at HEAD before the fix)uv run pytest docs/adr/defadr_doeff_core_001_spawn_boundary_inheritance.hy -q→ 2 passedtest_spawn_with_handlers,test_scheduler,test_with_observe_visibility,test_lazy_ask_inner_handler_propagation,test_get_handlers_defk+ new file + ADR) → 68 passeduv run pytest -q→ 959 passed, 87 skipped, 1 failed — the single failure is pre-existing at base (see below)uv run pyright doeff/→ 0 errors;semgrep --config .semgrep.yaml doeff/ packages/ --error→ pass;doeff-linter→ 0 errors on changed files;cargo test(doeff-vm-core) /cargo check(doeff-vm) → passuv sync --reinstall-package doeff-vmwith cargo on PATH) before all test runs, per AGENTS.md stale-build warningPre-existing failures (not from this branch)
tests/architecture/test_no_public_withhandler_shim.py::test_public_withhandler_shim_imports_are_gonefails at basecacc4b85: the fixture string(import doeff [WithHandler])insidepackages/doeff-adr/tests/test_defadr_macros.py(commit c6bf954) trips the shim-guard regex (commit b1fcca0). Neither file is touched by this branch.make lint-ruffhas 20 pre-existing errors in files untouched by this branch (doeff-agents, doeff-conductor, memo tests,doeff/program.py:77SIM108). The 1 ruff error introduced by this branch (import sort in the new test file) was fixed.Follow-up
doeff-traversehandlers use the sameget_inner_handlerscapture pattern for its parallel workers and still drop observers; migrating them toget_inner_boundariesis a natural follow-up (kept out of scope here to match the issue's Spawn scope).Issue: scheduler-spawn-drops-observer-boundary
🤖 Generated with Claude Code