Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton fb8d775fec fix(core): reject unknown session interrupts 2026-07-16 10:50:11 -04:00
3 changed files with 35 additions and 13 deletions
+7 -2
View File
@@ -276,7 +276,7 @@ export interface Interface {
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
readonly background: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
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) {
+22 -8
View File
@@ -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 {
+6 -3
View File
@@ -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([])
}),
)