Browse Source

fix(session): distinguish idle reasons for completion and abort

Kit Langton 1 tháng trước cách đây
mục cha
commit
8084f9dfd8

+ 1 - 1
packages/opencode/src/server/routes/session.ts

@@ -376,7 +376,7 @@ export const SessionRoutes = lazy(() =>
         }),
       ),
       async (c) => {
-        SessionPrompt.cancel(c.req.valid("param").sessionID)
+        SessionPrompt.cancel(c.req.valid("param").sessionID, "aborted")
         return c.json(true)
       },
     )

+ 4 - 1
packages/opencode/src/session/processor.ts

@@ -381,7 +381,10 @@ export namespace SessionProcessor {
                 sessionID: input.assistantMessage.sessionID,
                 error: input.assistantMessage.error,
               })
-              SessionStatus.set(input.sessionID, { type: "idle" })
+              SessionStatus.set(input.sessionID, {
+                type: "idle",
+                reason: error.name === "MessageAbortedError" ? "aborted" : "error",
+              })
             }
           }
           if (snapshot) {

+ 26 - 7
packages/opencode/src/session/prompt.ts

@@ -254,17 +254,21 @@ export namespace SessionPrompt {
     return s[sessionID].abort.signal
   }
 
-  export function cancel(sessionID: string) {
+  export function cancel(sessionID: string, reason: SessionStatus.IdleReason = "aborted") {
     log.info("cancel", { sessionID })
+    const idle = () => {
+      if (SessionStatus.get(sessionID).type === "idle") return
+      SessionStatus.set(sessionID, { type: "idle", reason })
+    }
     const s = state()
     const match = s[sessionID]
     if (!match) {
-      SessionStatus.set(sessionID, { type: "idle" })
+      idle()
       return
     }
     match.abort.abort()
     delete s[sessionID]
-    SessionStatus.set(sessionID, { type: "idle" })
+    idle()
     return
   }
 
@@ -283,7 +287,8 @@ export namespace SessionPrompt {
       })
     }
 
-    using _ = defer(() => cancel(sessionID))
+    let reason: SessionStatus.IdleReason = "completed"
+    using _ = defer(() => cancel(sessionID, reason))
 
     // Structured output state
     // Note: On session resumption, state is reset but outputFormat is preserved
@@ -295,7 +300,10 @@ export namespace SessionPrompt {
     while (true) {
       SessionStatus.set(sessionID, { type: "busy" })
       log.info("loop", { step, sessionID })
-      if (abort.aborted) break
+      if (abort.aborted) {
+        reason = "aborted"
+        break
+      }
       let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
 
       let lastUser: MessageV2.User | undefined
@@ -536,7 +544,10 @@ export namespace SessionPrompt {
           auto: task.auto,
           overflow: task.overflow,
         })
-        if (result === "stop") break
+        if (result === "stop") {
+          reason = abort.aborted ? "aborted" : "completed"
+          break
+        }
         continue
       }
 
@@ -698,11 +709,19 @@ export namespace SessionPrompt {
             retries: 0,
           }).toObject()
           await Session.updateMessage(processor.message)
+          reason = "error"
           break
         }
       }
 
-      if (result === "stop") break
+      if (result === "stop") {
+        if (processor.message.error?.name === "MessageAbortedError") {
+          reason = "aborted"
+        } else if (processor.message.error) {
+          reason = "error"
+        }
+        break
+      }
       if (result === "compact") {
         await SessionCompaction.create({
           sessionID,

+ 9 - 3
packages/opencode/src/session/status.ts

@@ -4,10 +4,14 @@ import { Instance } from "@/project/instance"
 import z from "zod"
 
 export namespace SessionStatus {
+  export const IdleReason = z.enum(["completed", "aborted", "error"])
+  export type IdleReason = z.infer<typeof IdleReason>
+
   export const Info = z
     .union([
       z.object({
         type: z.literal("idle"),
+        reason: IdleReason.optional(),
       }),
       z.object({
         type: z.literal("retry"),
@@ -65,9 +69,11 @@ export namespace SessionStatus {
     })
     if (status.type === "idle") {
       // deprecated
-      Bus.publish(Event.Idle, {
-        sessionID,
-      })
+      if (!status.reason || status.reason === "completed") {
+        Bus.publish(Event.Idle, {
+          sessionID,
+        })
+      }
       delete state()[sessionID]
       return
     }

+ 1 - 1
packages/opencode/src/tool/task.ts

@@ -119,7 +119,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
       const messageID = Identifier.ascending("message")
 
       function cancel() {
-        SessionPrompt.cancel(session.id)
+        SessionPrompt.cancel(session.id, "aborted")
       }
       ctx.abort.addEventListener("abort", cancel)
       using _ = defer(() => ctx.abort.removeEventListener("abort", cancel))

+ 1 - 0
packages/sdk/js/src/gen/types.gen.ts

@@ -453,6 +453,7 @@ export type EventPermissionReplied = {
 export type SessionStatus =
   | {
       type: "idle"
+      reason?: "completed" | "aborted" | "error"
     }
   | {
       type: "retry"

+ 1 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -581,6 +581,7 @@ export type EventPermissionReplied = {
 export type SessionStatus =
   | {
       type: "idle"
+      reason?: "completed" | "aborted" | "error"
     }
   | {
       type: "retry"