From fb8d775fecd2d88c489c9a640463b160ef19a1b4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Jul 2026 10:50:11 -0400 Subject: [PATCH] fix(core): reject unknown session interrupts --- packages/core/src/session.ts | 9 +++++-- packages/core/src/tool/subagent.ts | 30 +++++++++++++++++------ packages/core/test/session-prompt.test.ts | 9 ++++--- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 34ff7ecfa9..d7672f5f73 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -276,7 +276,7 @@ export interface Interface { readonly active: Effect.Effect> readonly background: (sessionID: SessionSchema.ID) => Effect.Effect readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect - readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect + readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect readonly synthetic: (input: { id?: SessionMessage.ID sessionID: SessionSchema.ID @@ -804,7 +804,12 @@ const layer = Layer.effect( ), ), interrupt: Effect.fn("V2Session.interrupt")((sessionID) => - Effect.uninterruptible(execution.interrupt(sessionID)), + Effect.uninterruptible( + Effect.gen(function* () { + yield* result.get(sessionID) + yield* execution.interrupt(sessionID) + }), + ), ), revert: { stage: Effect.fn("V2Session.revert.stage")(function* (input) { diff --git a/packages/core/src/tool/subagent.ts b/packages/core/src/tool/subagent.ts index 1e459a674e..9c87aecaff 100644 --- a/packages/core/src/tool/subagent.ts +++ b/packages/core/src/tool/subagent.ts @@ -188,7 +188,13 @@ export const Plugin = { yield* runtime.session.prompt({ sessionID: child.id, text: input.prompt, resume: false }) yield* runtime.session.resume(child.id) return yield* latestAssistantText(child.id) - }).pipe(Effect.onInterrupt(() => runtime.session.interrupt(child.id))) + }).pipe( + Effect.onInterrupt(() => + runtime.session + .interrupt(child.id) + .pipe(Effect.catchTag("Session.NotFoundError", () => Effect.void)), + ), + ) const info = yield* runtime.job.start({ id: child.id, @@ -208,13 +214,21 @@ export const Plugin = { } } - const result = yield* runtime.job.block({ id: child.id, sessionID: context.sessionID }).pipe( - Effect.onInterrupt(() => - Effect.all([runtime.session.interrupt(child.id), runtime.job.cancel(child.id)], { - discard: true, - }), - ), - ) + const result = yield* runtime.job + .block({ id: child.id, sessionID: context.sessionID }) + .pipe( + Effect.onInterrupt(() => + Effect.all( + [ + runtime.session + .interrupt(child.id) + .pipe(Effect.catchTag("Session.NotFoundError", () => Effect.void)), + runtime.job.cancel(child.id), + ], + { discard: true }, + ), + ), + ) if (result?.type === "backgrounded") { yield* notifyWhenDone(context.sessionID, child.id, agent.name, input.description) return { diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 7ea2633f62..450fe94c64 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -156,13 +156,16 @@ describe("SessionV2.prompt", () => { }), ) - it.effect("delegates interruption without requiring a recorded Session", () => + it.effect("rejects interruption for an unknown Session", () => Effect.gen(function* () { const session = yield* SessionV2.Service interruptCalls.length = 0 + const missing = SessionV2.ID.make("ses_missing") - yield* session.interrupt(SessionV2.ID.make("ses_missing")) - expect(interruptCalls).toEqual([SessionV2.ID.make("ses_missing")]) + expect(yield* session.interrupt(missing).pipe(Effect.flip)).toEqual( + new SessionV2.NotFoundError({ sessionID: missing }), + ) + expect(interruptCalls).toEqual([]) }), )