refactor(core): every registry register-method returns the exact effect disposer
The exact-disposer fix (5fbac8be B1) repaired agents.register but left the same wrapper (return () => void dispose()) at seven sibling sites: tools.register, tools.restrict, systemPrompt.section/tools/variable, agents.setFactory, and subagents.registerProvider. A wrapper makes correct composite usage unrepresentable — the exact disposer cannot be recovered, so a generator effect yielding it leaves the inner effect disposing as a CONCURRENT SIBLING on owner unload, silently reproducing B1's ordering corruption. The exact disposer serves both usages (composite-nestable AND fire-and-forget callable); all seven now return it, typed () => Promise<void> | void, with the convention pinned by a discriminating test: an async-link composite probe that passes with the exact disposer and observes the sibling unregistration firing mid-drain with a wrapper. Re-auditing also surfaced that B1 itself SHIPPED a full-lint failure: it changed register()'s return type without updating cross-file consumers (agent.spec.ts dispose() statements, tool-bash's disposer list), which the staged-scoped pre-commit lint never saw — pnpm run lint was red at HEAD. Those three sites and this change's own fallout are fixed together: tests now await disposers (stronger — they observe the full unwind), sync paths void them, and the two annotation sites carry the honest union type. agents.register's README line had drifted the same way (B1 updated the JSDoc, not the README) — all seven README signatures now match; services catalog regenerated.
This commit is contained in:
@@ -544,7 +544,7 @@ describe('in-process structured output', () => {
|
||||
const run = ctx.subagents.start('spawn', structuredRequest(parent))
|
||||
// A backend hot-reload mid-run must not unregister the capture tool out
|
||||
// from under the live child: the registration rides the CHILD's fiber.
|
||||
disposeProvider()
|
||||
await disposeProvider()
|
||||
const result = await run.result
|
||||
expect(result.structured).toEqual({ answer: 4 })
|
||||
const child = ctx.agents.get(run.id)!
|
||||
|
||||
@@ -164,9 +164,11 @@ export class SubagentService extends Service {
|
||||
* the registration and `subagent/provider-removed` on unregistration, so
|
||||
* consumers can mirror provider lifecycle instead of assuming load order.
|
||||
* @param provider - the provider; its `name` is the registry key.
|
||||
* @returns the disposer that unregisters the provider.
|
||||
* @returns the disposer that unregisters the provider. The exact
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
registerProvider(provider: SubagentProvider): () => void {
|
||||
registerProvider(provider: SubagentProvider): () => Promise<void> | void {
|
||||
const dispose = this.ctx.effect(function* (this: SubagentService) {
|
||||
if (this.providers.has(provider.name)) {
|
||||
throw new SubagentError(`a subagent provider named "${provider.name}" is already registered`, 'DUPLICATE_PROVIDER')
|
||||
@@ -184,9 +186,13 @@ export class SubagentService extends Service {
|
||||
}
|
||||
this.ctx.emit('subagent/provider-added', provider)
|
||||
}.bind(this), 'subagents.registerProvider()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
return () => void dispose()
|
||||
// The EXACT cordis effect disposer, not a wrapper: a composite (generator)
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
return dispose
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('SubagentService', () => {
|
||||
expect(added).toEqual(['alpha'])
|
||||
expect(removed).toEqual([])
|
||||
|
||||
dispose()
|
||||
await dispose()
|
||||
expect(removed).toEqual(['alpha'])
|
||||
})
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('SubagentService', () => {
|
||||
ctx.on('subagent/provider-removed', name => void heard.push(name))
|
||||
|
||||
const dispose = ctx.subagents.registerProvider(new StubProvider('alpha'))
|
||||
expect(() => { dispose() }).not.toThrow()
|
||||
expect(() => void dispose()).not.toThrow()
|
||||
expect(heard).toEqual(['alpha']) // the listener AFTER the thrower still ran
|
||||
expect(ctx.subagents.getProvider('alpha')).toBeUndefined() // teardown reached quiescence
|
||||
expect(warnings.some(w => w.includes('boom removed listener'))).toBe(true)
|
||||
@@ -167,12 +167,12 @@ describe('SubagentService', () => {
|
||||
|
||||
const dispose = ctx.subagents.registerProvider(new StubProvider('reuse'))
|
||||
expect(ctx.subagents.list()).toEqual(['reuse'])
|
||||
dispose()
|
||||
await dispose()
|
||||
expect(ctx.subagents.list()).toEqual([])
|
||||
|
||||
const disposeAgain = ctx.subagents.registerProvider(new StubProvider('reuse'))
|
||||
expect(ctx.subagents.list()).toEqual(['reuse'])
|
||||
disposeAgain()
|
||||
await disposeAgain()
|
||||
expect(ctx.subagents.list()).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// available — deriving the wording from THAT provider — and unregister it
|
||||
// when the provider goes away, so the description can never outlive or
|
||||
// predate the provider it describes.
|
||||
let disposeTool: (() => void) | undefined
|
||||
let disposeTool: (() => Promise<void> | void) | undefined
|
||||
const mount = (provider: SubagentProvider): void => {
|
||||
const wording = providerWording(provider.inheritsParentContext)
|
||||
disposeTool = ctx.tools.register(defineTool({
|
||||
@@ -284,7 +284,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
})
|
||||
ctx.on('subagent/provider-removed', (name) => {
|
||||
if (name !== config.provider || disposeTool === undefined) return
|
||||
disposeTool()
|
||||
void disposeTool()
|
||||
disposeTool = undefined
|
||||
})
|
||||
const present = ctx.subagents.getProvider(config.provider)
|
||||
|
||||
Reference in New Issue
Block a user