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:
@@ -208,9 +208,16 @@ export class AgentRegistry extends Service {
|
||||
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
|
||||
* requires passing the carrier). Returns the disposer.
|
||||
* @param agent - the already-constructed agent to record in the store.
|
||||
* @returns the disposer that removes the agent and emits `agent/disposed`.
|
||||
* @returns the EXACT Cordis effect disposer (single-shot; a repeat call
|
||||
* returns undefined without awaiting an in-flight teardown). Exact
|
||||
* identity is load-bearing: a composite (generator) effect that owns a
|
||||
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
|
||||
* function so Cordis nests the unregistration at that yield position;
|
||||
* yielding a wrapper would leave it disposing as a concurrent sibling on
|
||||
* owner unload, unregistering the agent (and emitting `agent/disposed`)
|
||||
* while its final turn is still draining.
|
||||
*/
|
||||
register(agent: Agent): () => void {
|
||||
register(agent: Agent): () => Promise<void> | void {
|
||||
const dispose = this.ctx.effect(function* (this: AgentRegistry) {
|
||||
if (this.store.has(agent.id)) {
|
||||
throw new Error(`agent "${agent.id}" is already registered`)
|
||||
@@ -242,9 +249,7 @@ export class AgentRegistry extends Service {
|
||||
}
|
||||
this.ctx.emit(scopeTarget(agent, agent), 'agent/created', agent)
|
||||
}.bind(this), 'agents.register()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
return () => void dispose()
|
||||
return dispose
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user