mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(session): rename interrupt resume option
This commit is contained in:
@@ -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"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"]>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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* (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user