refactor(session): rename interrupt resume option

This commit is contained in:
Dax Raad
2026-09-14 13:32:43 -04:00
parent 3468a092c9
commit 6bc60caab9
23 changed files with 50 additions and 51 deletions
+4 -4
View File
@@ -162,10 +162,10 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [x] 061 | `POST` | `/api/experimental/session/{sessionID}/skill` | `experimental.session.skill` | Experimental-only | Skill ID is now the `id` field; standalone activation remains experimental. |
| [x] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | Keep | Durable synthetic admission and delivery controls retained. |
| [x] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | Change | Caller ID is now the optimistic shell message ID; server derives its event ID. |
| [ ] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | | |
| [ ] 065 | `POST` | `/api/session/{sessionID}/wait` | `session.wait` | | |
| [ ] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | | |
| [ ] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | | |
| [x] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | Keep | Durable compaction admission and delivery controls retained. |
| [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. |
| [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. |
| [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. |
| [ ] 068 | `PUT` | `/api/session/{sessionID}/environment` | `session.environment` | | |
| [ ] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | | |
@@ -187,7 +187,7 @@ function ComposerStory(props: {
)
},
onStop: () =>
setStory("activity", props.continueOnStop ? "POST /interrupt · continue: true" : "Stop requested"),
setStory("activity", props.continueOnStop ? "POST /interrupt · resume: true" : "Stop requested"),
},
},
})
+1 -1
View File
@@ -37,7 +37,7 @@ export function createActiveComposerAdapter(input: {
}),
interrupt: () =>
server.api.session
.interrupt({ sessionID: id, continue: true })
.interrupt({ sessionID: id, resume: true })
.then(() => undefined)
.catch(() => undefined),
}
+1 -1
View File
@@ -1307,7 +1307,7 @@ export type SessionLogOutput =
| EventLog.Synced
export type SessionLogOperation<E = never> = (input: SessionLogInput) => Stream.Stream<SessionLogOutput, E>
export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly continue?: boolean | undefined }
export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly resume?: boolean | undefined }
export type SessionInterruptOutput = { readonly interrupted: boolean }
export type SessionInterruptOperation<E = never> = (
input: SessionInterruptInput,
@@ -668,10 +668,9 @@ const EndpointSessionLog = (raw: RawClient["server.session"]) => (input: Session
const EndpointSessionInterrupt = (raw: RawClient["server.session"]) => (input: SessionInterruptInput) =>
preserveEffect<SessionInterruptOutput>()(
raw["session.interrupt"]({
params: { sessionID: input["sessionID"] },
query: { continue: input["continue"] },
}).pipe(Effect.mapError(mapClientError)),
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] }, query: { resume: input["resume"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const EndpointSessionBackground = (raw: RawClient["server.session"]) => (input: SessionBackgroundInput) =>
@@ -767,7 +767,7 @@ export function make(options: ClientOptions) {
request<SessionWaitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 503],
empty: true,
@@ -946,7 +946,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
query: { continue: input["continue"] },
query: { resume: input["resume"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -4381,7 +4381,7 @@ export type SessionLogOutput = SessionLogItem
export type SessionInterruptInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly continue?: { readonly continue?: boolean | undefined }["continue"]
readonly resume?: { readonly resume?: boolean | undefined }["resume"]
}
export type SessionInterruptOutput = SessionInterruptResponse
+1 -1
View File
@@ -883,7 +883,7 @@ test("session methods use the public HTTP contract", async () => {
["POST", "http://localhost:3000/api/session/ses_test/generate"],
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
["POST", "http://localhost:3000/api/session/ses_test/compact"],
["POST", "http://localhost:3000/api/session/ses_test/wait"],
["POST", "http://localhost:3000/api/experimental/session/ses_test/wait"],
["GET", "http://localhost:3000/api/session/ses_test/context"],
["GET", "http://localhost:3000/api/experimental/session/ses_test/log?after=0"],
["POST", "http://localhost:3000/api/session/ses_test/interrupt?continue=true"],
+1 -1
View File
@@ -549,7 +549,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
synthetic: sessions.synthetic,
interrupt: (input) =>
sessions
.interrupt(input.sessionID, { continue: input.continue })
.interrupt(input.sessionID, { resume: input.resume })
.pipe(Effect.map((interrupted) => ({ interrupted }))),
wait: (input) => sessions.wait(input.sessionID),
context: (input) => sessions.context(input.sessionID),
+1 -1
View File
@@ -205,7 +205,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, options?: { readonly continue?: boolean }) => Effect.Effect<boolean>
readonly interrupt: (sessionID: SessionSchema.ID, options?: { readonly resume?: boolean }) => Effect.Effect<boolean>
readonly synthetic: (
input: Parameters<Session.Handle["synthetic"]>[0] & { sessionID: SessionSchema.ID },
) => ReturnType<Session.Handle["synthetic"]>
+2 -2
View File
@@ -34,7 +34,7 @@ export interface Interface {
readonly interrupt: (
sessionID: SessionSchema.ID,
options?: {
readonly continue?: boolean
readonly resume?: boolean
readonly reason?: "user" | "inactivity"
readonly awaitSettlement?: boolean
},
@@ -155,7 +155,7 @@ export const layer = Layer.effect(
interrupt: (sessionID, options) =>
Effect.gen(function* () {
const interrupted = yield* coordinator.interrupt(sessionID, options?.reason ?? "user", options)
if (!options?.continue) return interrupted
if (!options?.resume) return interrupted
// Resume steering input and between-turn control work from the interrupted
// intent. Queued next-turn prompts stay parked: a steer-scoped drain never
// promotes them, and a control item behind a queued prompt waits its turn.
+1 -1
View File
@@ -307,7 +307,7 @@ export const make = Effect.fn("Session.make")(function* () {
),
)
const interrupt = Effect.fn("Session.interrupt")(
(sessionID: SessionSchema.ID, options?: { readonly continue?: boolean }) =>
(sessionID: SessionSchema.ID, options?: { readonly resume?: boolean }) =>
Effect.uninterruptible(execution.interrupt(sessionID, options)),
)
const stage = Effect.fn("Session.revert.stage")(function* (
+5 -5
View File
@@ -1104,7 +1104,7 @@ describe("SessionExecution interrupt continuation", () => {
yield* execution.resume(sessionID).pipe(Effect.forkScoped)
yield* Deferred.await(draining)
yield* execution.interrupt(sessionID, { continue: true })
yield* execution.interrupt(sessionID, { resume: true })
yield* execution.awaitIdle(sessionID)
// The successor drain is steer-scoped: queued next-turn work stays parked.
@@ -1136,7 +1136,7 @@ describe("SessionExecution interrupt continuation", () => {
yield* execution.resume(sessionID).pipe(Effect.forkScoped)
yield* Deferred.await(draining)
yield* execution.interrupt(sessionID, { continue: true })
yield* execution.interrupt(sessionID, { resume: true })
yield* execution.awaitIdle(sessionID)
expect(drains).toEqual(["input"])
@@ -1159,7 +1159,7 @@ describe("SessionExecution interrupt continuation", () => {
)
const execution = Context.get(context, SessionExecution.Service)
yield* execution.interrupt(sessionID, { continue: true })
yield* execution.interrupt(sessionID, { resume: true })
yield* execution.awaitIdle(sessionID)
expect(drains).toEqual([{ force: false, promotable: "steer" }])
@@ -1188,7 +1188,7 @@ describe("SessionExecution interrupt continuation", () => {
yield* execution.resume(sessionID).pipe(Effect.forkScoped)
yield* Deferred.await(draining)
yield* execution.interrupt(sessionID, { continue: true })
yield* execution.interrupt(sessionID, { resume: true })
yield* execution.awaitIdle(sessionID)
// Control work is housekeeping, not next-turn input: continue runs it.
@@ -1214,7 +1214,7 @@ describe("SessionExecution interrupt continuation", () => {
)
const execution = Context.get(context, SessionExecution.Service)
yield* execution.interrupt(sessionID, { continue: true })
yield* execution.interrupt(sessionID, { resume: true })
yield* execution.awaitIdle(sessionID)
// The queued prompt is next in line; the compaction behind it waits its turn.
+3 -3
View File
@@ -615,7 +615,7 @@ describe("Session-owned handles", () => {
const joining = yield* Deferred.make<void>()
const drains: SessionSchema.ID[] = []
const resumes: SessionSchema.ID[] = []
const interrupts: Array<{ sessionID: SessionSchema.ID; options?: { readonly continue?: boolean } }> = []
const interrupts: Array<{ sessionID: SessionSchema.ID; options?: { readonly resume?: boolean } }> = []
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, never>({
drain: (id) =>
Effect.sync(() => void drains.push(id)).pipe(
@@ -656,10 +656,10 @@ describe("Session-owned handles", () => {
yield* fixture.sessions.forSession(sessionID).wait()
expect(drains).toEqual([sessionID])
expect(yield* coordinator.active).toEqual(new Set())
expect(yield* fixture.sessions.forSession(sessionID).interrupt({ continue: true })).toBe(false)
expect(yield* fixture.sessions.forSession(sessionID).interrupt({ resume: true })).toBe(false)
expect(yield* fixture.sessions.forSession(sessionID).interrupt()).toBe(false)
expect(interrupts).toEqual([
{ sessionID, options: { continue: true } },
{ sessionID, options: { resume: true } },
{ sessionID, options: undefined },
])
expect(fixture.locations).toEqual([])
+2 -2
View File
@@ -51,7 +51,7 @@ const execution = Layer.succeed(
interrupt: (sessionID, options) =>
Effect.sync(() => {
interruptCalls.push(sessionID)
interruptContinuations.push(options?.continue)
interruptContinuations.push(options?.resume)
return activeSessions.delete(sessionID)
}),
wake: (sessionID) =>
@@ -211,7 +211,7 @@ describe("Session.prompt", () => {
interruptContinuations.length = 0
wakeCalls.length = 0
yield* session.interrupt(sessionID, { continue: true })
yield* session.interrupt(sessionID, { resume: true })
expect(interruptCalls).toEqual([sessionID])
expect(interruptContinuations).toEqual([true])
+4 -4
View File
@@ -2605,10 +2605,10 @@
}
}
},
"/api/session/{sessionID}/wait": {
"/api/experimental/session/{sessionID}/wait": {
"post": {
"tags": ["session"],
"operationId": "session.wait",
"operationId": "experimental.session.wait",
"parameters": [
{
"name": "sessionID",
@@ -4005,7 +4005,7 @@
"required": true
},
{
"name": "continue",
"name": "resume",
"in": "query",
"schema": {
"anyOf": [
@@ -4071,7 +4071,7 @@
}
}
},
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When continue=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When resume=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"summary": "Interrupt session execution"
}
},
+4 -4
View File
@@ -486,7 +486,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.post("session.wait", "/api/session/:sessionID/wait", {
HttpApiEndpoint.post("session.wait", "/api/experimental/session/:sessionID/wait", {
params: { sessionID: Session.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, ServiceUnavailableError],
@@ -494,7 +494,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "session.wait",
identifier: "experimental.session.wait",
summary: "Wait for session",
description: "Wait for a session agent loop to become idle.",
}),
@@ -716,7 +716,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
.add(
HttpApiEndpoint.post("session.interrupt", "/api/session/:sessionID/interrupt", {
params: { sessionID: Session.ID },
query: { continue: BooleanFromString.pipe(Schema.optional) },
query: { resume: BooleanFromString.pipe(Schema.optional) },
success: Schema.Struct({
interrupted: Schema.Boolean.annotate({
description: "Whether an active execution owned by this OpenCode process was interrupted.",
@@ -730,7 +730,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
identifier: "session.interrupt",
summary: "Interrupt session execution",
description:
"Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When continue=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When resume=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
}),
),
)
+1 -1
View File
@@ -591,7 +591,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
.handle(
"session.interrupt",
Effect.fn(function* (ctx) {
return { interrupted: yield* session.interrupt(ctx.params.sessionID, { continue: ctx.query.continue }) }
return { interrupted: yield* session.interrupt(ctx.params.sessionID, { resume: ctx.query.resume }) }
}),
)
.handle(
+1 -1
View File
@@ -521,7 +521,7 @@ export function Prompt(props: PromptProps) {
if (store.interrupt >= 2) {
void client.api.session.interrupt({
sessionID: props.sessionID,
continue: true,
resume: true,
})
setStore("interrupt", 0)
}
+1 -1
View File
@@ -380,7 +380,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
void (
state.stream
? state.stream.then((item) => item.handle.interruptActiveTurn())
: state.sdk.session.interrupt({ sessionID: state.sessionID, continue: true })
: state.sdk.session.interrupt({ sessionID: state.sessionID, resume: true })
).catch(() => {})
return true
},
+2 -2
View File
@@ -1618,7 +1618,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.wait = active
const interrupt = () => {
active.interrupted = true
void sdk.session.interrupt({ sessionID: input.sessionID, continue: true }).catch(() => {})
void sdk.session.interrupt({ sessionID: input.sessionID, resume: true }).catch(() => {})
}
next.signal?.addEventListener("abort", interrupt, { once: true })
try {
@@ -1864,7 +1864,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
// A failed request paints nothing, so the two-press gesture stays available for retry,
// and a lifecycle event racing the ack wins via the epoch guard.
const epoch = state.executionEpoch
await sdk.session.interrupt({ sessionID: input.sessionID, continue: true }).then(
await sdk.session.interrupt({ sessionID: input.sessionID, resume: true }).then(
() => {
if (state.executionEpoch === epoch) paintIdle(blockerStatus(state.view))
},
+4 -4
View File
@@ -2605,10 +2605,10 @@
}
}
},
"/api/session/{sessionID}/wait": {
"/api/experimental/session/{sessionID}/wait": {
"post": {
"tags": ["session"],
"operationId": "session.wait",
"operationId": "experimental.session.wait",
"parameters": [
{
"name": "sessionID",
@@ -4005,7 +4005,7 @@
"required": true
},
{
"name": "continue",
"name": "resume",
"in": "query",
"schema": {
"anyOf": [
@@ -4071,7 +4071,7 @@
}
}
},
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When continue=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When resume=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"summary": "Interrupt session execution"
}
},
+4 -4
View File
@@ -2605,10 +2605,10 @@
}
}
},
"/api/session/{sessionID}/wait": {
"/api/experimental/session/{sessionID}/wait": {
"post": {
"tags": ["session"],
"operationId": "session.wait",
"operationId": "experimental.session.wait",
"parameters": [
{
"name": "sessionID",
@@ -4005,7 +4005,7 @@
"required": true
},
{
"name": "continue",
"name": "resume",
"in": "query",
"schema": {
"anyOf": [
@@ -4071,7 +4071,7 @@
}
}
},
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When continue=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"description": "Interrupt active execution owned by this OpenCode process. Returns interrupted=true when an active execution was interrupted and false for the idle no-op. When resume=true, execution resumes pending steering input and next-in-line control items (manual compaction, moves) while queued prompts remain parked.",
"summary": "Interrupt session execution"
}
},