Skip to content

Spawn: inherit WithObserve boundaries via GetObservers(k) (scheduler-spawn-drops-observer-boundary) - #478

Closed
proboscis wants to merge 2 commits into
mainfrom
issue/scheduler-spawn-drops-observer-boundary/run-20260705-004313
Closed

Spawn: inherit WithObserve boundaries via GetObservers(k) (scheduler-spawn-drops-observer-boundary)#478
proboscis wants to merge 2 commits into
mainfrom
issue/scheduler-spawn-drops-observer-boundary/run-20260705-004313

Conversation

@proboscis

Copy link
Copy Markdown
Owner

Issue

scheduler-spawn-drops-observer-boundary — Spawn は inner handler を継承するのに intercept/observer 境界を落とすため、scheduled() の内側に置いた WithObserve が Spawn した子タスクの効果を一切観測できない(silent drop)。

設計判断(issue の論点 1–3 への回答)

論点 2 の機構で論点 1 の結果を実現した:

  1. VM コアは方針を持たない: observer は従来どおり fiber チェーンスコープ。VM が暗黙に境界を伝搬することはない。
  2. GetObservers(k) を新設: GetHandlers(k) の observer 版。継続の detached fiber chain(perform サイト→捕捉ハンドラ)上の intercept 境界の observer callable を innermost 順で返す。非消費(one-shot 継続に影響なし)。
  3. scheduler が opt-in: Spawn ハンドラが get_inner_observers(k) で捕捉し、タスク初回実行時に WithObserve で子プログラムを再ラップ。handler 継承と完全に対称。
  4. 所有権/解放(論点 3): 再インストールされた境界は子タスク自身の fiber tree 内に存在し、タスクの terminal 化(completed/failed/cancelled)とともに _release_task_refs で解放される。継承ハンドラと同一のライフサイクルで、cancel の特別扱いは不要。multi-shot は本 VM に存在しない(SPEC-VM-021 one-shot)、かつ捕捉は read-only。

仕様は specs/vm/SPEC-SCHED-001-cooperative-scheduling.mdObserver inheritance 節に明文化した。

変更内容

レイヤ 変更
doeff-vm-core DetachedFiberChain::observer_callables / Continuation::observer_callablesDoCtrl::GetObservers { from } + observers_in_caller_chain(GetHandlers と対称)
doeff-vm (PyO3) PyGetObservers pyclass、classify の eager 変換 continuation_observers_value、モジュール登録、__init__.py/.pyi
doeff GetObservers re-export、handler_utils.get_inner_observers
doeff-core-effects/scheduler Spawn で observer 捕捉 → alloc_task(inner_observers=…) → 初回実行時に WithObserve 再ラップ、_release_task_refs で解放
specs SPEC-SCHED-001 に Observer inheritance 節を追加

TDD 順守: commit 89cb8fa で失敗するテストを先にコミット(4 failed / 2 passed のガード)、次コミットで実装。

Verification(1:1 マッピング)

Issue に形式的な ## Verification 節は無いため、issue の再現スクリプトの期待値と論点を基準にマップする:

Issue の検証項目 テスト
ケース A: scheduled(WithObserve(obs, …)) で child-effect が観測される tests/test_spawn_observer_inheritance.py::test_observer_inside_scheduler_sees_spawned_task_effects
ケース B: 外側 observer は従来どおり全部見える(回帰ガード) tests/test_spawn_observer_inheritance.py::test_observer_outside_scheduler_sees_spawned_task_effects
継承の再帰(孫タスク) tests/test_spawn_observer_inheritance.py::test_observer_inside_scheduler_sees_nested_spawn_effects
spawn サイトを囲まない observer は継承されない(スコープ規則) tests/test_spawn_observer_inheritance.py::test_observer_scoped_below_spawn_site_is_not_inherited
論点 3: cancel 時の境界解放 tests/test_spawn_observer_inheritance.py::test_cancelled_child_effects_before_cancel_are_observed
GetObservers(k) intrinsic 単体 tests/test_spawn_observer_inheritance.py::test_get_inner_observers_returns_observer_callables

Issue 再現スクリプト実行結果(修正後)

A: scheduled(WithObserve(obs, mark_handler(main)))
  result=1 observed=['main-effect', 'child-effect']   ← 修正前は ['main-effect'] のみ
B: WithObserve(obs, scheduled(mark_handler(main)))
  result=1 observed=['main-effect', 'child-effect']

追加のランタイム検証(public API 経由の probe)

probe1 gather: result=['a', 'b'] observed=['work-a', 'work-b']
probe2 failing child: result=caught:boom observed=['before-crash']
probe3 raising observer: result=1        (observer 例外はプログラムに影響しない: 既存 VM 契約)
probe4 nested observers: result=1 outer=['c1'] inner=['c1']
probe5 no observer: result=1             (observer 無しの Spawn は無変化)

テスト / lint

  • uv run pytest 全体: 959 passed, 1 failed — 唯一の failure tests/architecture/test_no_public_withhandler_shim.py::test_public_withhandler_shim_imports_are_gone は base cacc4b8 でも失敗する既存問題(packages/doeff-adr/tests/test_defadr_macros.py を検出。本 PR と無関係)。
  • cargo test(doeff-vm-core): passed。
  • make lint-pyright: 0 errors。
  • ruff: 本 PR の変更ファイルは clean(doeff/program.py の SIM108 は base から存在する既存指摘)。
  • semgrep / doeff-linter: 本 PR 変更ファイルの指摘件数は base と同一(新規指摘ゼロ; scheduler.py は before/after ともに 5 errors / 43 warnings / 39 info)。リポジトリ全体には既存指摘が多数あり本 PR のスコープ外。

Verification deviations

  • Semgrep ルール追加なし: 本変更は挙動の追加であり、旧 API の禁止・除去を伴わないため、AGENTS.md の TDD+Semgrep プロトコル Phase 2(ban ルール)は対象外と判断した。
  • issue の「multi-shot continuation の扱い」は本 VM が one-shot 継続のみ(SPEC-VM-021)のためテスト対象が存在しない。仕様節に capture が read-only である旨を明記した。

🤖 Generated with Claude Code

proboscis and others added 2 commits July 5, 2026 00:55
Spawn inherits inner handlers but silently drops WithObserve boundaries,
so observers inside scheduled() never see spawned-task effects.
4 tests fail at HEAD (inside-scheduler, nested, cancel, GetObservers
intrinsic); 2 pass as regression guards (outside-scheduler, sibling scope).
Spec section documents the decided semantics.

Issue: scheduler-spawn-drops-observer-boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rs(k)

Spawn captured inner handlers (GetHandlers) but dropped observer
boundaries, so observers installed inside scheduled() silently missed
all effects performed by spawned tasks.

- doeff-vm-core: DetachedFiberChain::observer_callables +
  Continuation::observer_callables; DoCtrl::GetObservers { from } +
  observers_in_caller_chain (symmetric to GetHandlers).
- doeff-vm: PyGetObservers pyclass, eager classify path
  (continuation_observers_value), module registration, stub.
- doeff: export GetObservers; handler_utils.get_inner_observers.
- scheduler: Spawn captures inner observers and reinstalls them with
  WithObserve around the child program; released with the task in
  _release_task_refs. Inheritance recurses across nested Spawn.

Observer semantics stay fiber-chain-scoped at the VM level; the
scheduler opts in, mirroring handler inheritance. Spec updated
(SPEC-SCHED-001 Observer inheritance).

Issue: scheduler-spawn-drops-observer-boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@proboscis

Copy link
Copy Markdown
Owner Author

Superseded by #479 (merged): same issue implemented and verified via GetBoundaries(k). This branch came from a run that was stopped mid-flight.

@proboscis proboscis closed this Jul 5, 2026
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.

1 participant