fix: honor the teardown order on owner unload; make the structured commit unconditional

Adversarial-review findings (own reviewer agent), each verified and pinned:

B1: agents.register() returned a wrapper lambda, so the factory composite's
yield could not identity-nest it — on OWNER unload the unregistration (and
agent/disposed) disposed as a concurrent sibling, firing mid-drain while
the final turn was still closing (pre-existing on master; this branch's
docs re-assert the order, so it must be true). register() now returns the
EXACT cordis effect disposer (the Scope.rawDispose move); the composite
nests it and owner unload runs stop/drain -> unregister -> detach -> scope
like every other path. Regression test pins turn-end before disposed
before detach on owner unload.

B2: the structured two-phase commit could promote a stale stage when a
later capture call REUSED the orphaned stage's call id with a body that
never staged (denied downstream, or invalid args throwing pre-stage). The
runtime's pre-execute listener now clears any stale stage unconditionally
when a new capture call enters the pipeline — only a call's own body can
stage for its commit; the call-id mismatch guard becomes a defensive
second layer. Repro test: blocked capture then same-id invalid call.

C1: an explicit empty toolFilter config now fails at plugin LOAD (the
check is self-contained) instead of killing every delegation at child
setup. C2: Scope.dispose/ScopeHost.dispose @returns state the single-shot
repeat-call semantics honestly.
This commit is contained in:
Tianyi Cui
2026-07-09 04:48:52 +08:00
parent 9ff8720da5
commit 547aacee2f
7 changed files with 122 additions and 10 deletions

View File

@@ -83,7 +83,12 @@ export interface Scope {
* returns undefined the second time; this wrapper Promise-normalizes it).
* After disposal the scoped context is inert — a further registration
* through it throws Cordis's INACTIVE_EFFECT.
* @returns resolves when every registration's disposer has settled.
* @returns for the call that initiates teardown: resolves when every
* registration's disposer has settled. A repeat/racing call resolves
* immediately WITHOUT awaiting the in-flight teardown (the underlying
* Cordis disposer is single-shot) — a caller needing a shared quiescence
* boundary across racing disposers keeps its own completion promise (the
* agent factory's pattern).
*/
dispose(): Promise<void>
}
@@ -250,7 +255,8 @@ export interface ScopeHost {
mint(key: ScopeKey): Scope
/**
* Dispose the host fiber and with it every scope minted through it.
* @returns resolves when all collected disposers have settled.
* @returns resolves when all collected disposers have settled (first call;
* a repeat call resolves immediately — single-shot, like Scope.dispose).
*/
dispose(): Promise<void>
}