fix(mode): guard suspended retry flush disposal
This commit is contained in:
@@ -234,6 +234,7 @@ export class ModesService extends Service {
|
||||
constructor(ctx: Context, config: ModeConfig = { modes: {} }) {
|
||||
super(ctx, 'modes')
|
||||
this.resolved = resolveConfig(config)
|
||||
let disposed = false
|
||||
|
||||
// Boundary flushes ride the loop's interception seams, NOT the
|
||||
// `session/event` feed: post-commit session observers are observe-only
|
||||
@@ -273,7 +274,10 @@ export class ModesService extends Service {
|
||||
next,
|
||||
) => {
|
||||
const decision = await next()
|
||||
if (decision.action !== 'retry') return decision
|
||||
// A waterfall can capture this wrapper before Cordis unregisters it.
|
||||
// Do not let that stale continuation mutate the session after its
|
||||
// owning plugin fiber has been disposed.
|
||||
if (disposed || decision.action !== 'retry') return decision
|
||||
try {
|
||||
this.onBoundary(agent.session, false)
|
||||
} catch (error) {
|
||||
@@ -281,6 +285,7 @@ export class ModesService extends Service {
|
||||
}
|
||||
return decision
|
||||
}, { prepend: true })
|
||||
ctx.effect(() => () => { disposed = true }, 'dsh-mode: close boundary lifetime')
|
||||
|
||||
ctx.on('agent/created', (agent) => {
|
||||
const seed = agent.options.mode
|
||||
|
||||
@@ -874,6 +874,30 @@ describe('exit_plan_mode', () => {
|
||||
})
|
||||
|
||||
describe('HMR disposal', () => {
|
||||
it('does not flush a retry boundary that resumes after plugin disposal', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
const fiber = await ctx.plugin(ModesService, PLAN_CONFIG)
|
||||
const agent = agentWithSession('disposed-in-flight-recovery')
|
||||
const recoveryEntered = Promise.withResolvers<true>()
|
||||
const releaseRecovery = Promise.withResolvers<true>()
|
||||
ctx.on('agent/request-error', async (_agent, _turn, _step, _error, _failure, _history, _signal, _next) => {
|
||||
recoveryEntered.resolve(true)
|
||||
await releaseRecovery.promise
|
||||
return { action: 'retry' }
|
||||
})
|
||||
ctx.modes.set(agent, PLAN_MODE)
|
||||
|
||||
const recovery = recoveryBoundary(ctx, agent, { action: 'fail' })
|
||||
await recoveryEntered.promise
|
||||
await fiber.dispose()
|
||||
releaseRecovery.resolve(true)
|
||||
|
||||
expect(await recovery).toEqual({ action: 'retry' })
|
||||
expect(agent.session.events.some(event => event.type === 'mode/set')).toBe(false)
|
||||
})
|
||||
|
||||
it('unregisters the service, listeners, prompt section, and stable exit tool with the plugin fiber', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
|
||||
Reference in New Issue
Block a user