diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 0b54dd240..af3898c6b 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -1331,6 +1331,17 @@ export const SessionRoutes = lazy(() => await AppRuntime.runPromise( Effect.gen(function* () { const sessions = yield* Session.Service + // Surface the route's declared 404 instead of silently succeeding: + // deleting a part that does not exist is a not-found, not a no-op. + // The check lives in the route, not Session.removePart, because + // removePart is also called internally by the message processor, + // which must stay a tolerant no-op for an already-gone part. + const part = yield* sessions.getPart({ + sessionID: params.sessionID, + messageID: params.messageID, + partID: params.partID, + }) + if (!part) throw new NotFoundError({ message: `Part not found: ${params.partID}` }) yield* sessions.removePart({ sessionID: params.sessionID, messageID: params.messageID, diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index f7fdfd2a8..c05ee6f4b 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -416,6 +416,14 @@ describe("session messages endpoint", () => { expect(await remove.json()).toBe(true) expect((await svc.messages({ sessionID: session.id }))[0].parts).toHaveLength(0) + // The route's declared 404 is now real: deleting an already-removed + // part surfaces NotFoundError instead of silently succeeding. + const missing = await app.request(`/session/${session.id}/message/${messageID}/part/${partID}`, { + method: "DELETE", + }) + expect(missing.status).toBe(404) + expect((await missing.json()).name).toBe("NotFoundError") + await svc.remove(session.id) }, }),