fix: use mapError instead of orDie for context snapshot decoding (#30905)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
This commit is contained in:
weiconghe 2026-06-05 20:37:00 +08:00 committed by GitHub
parent a136caa1b3
commit a261b55e43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 46 additions and 4 deletions

View File

@ -92,7 +92,7 @@ export class OperationUnavailableError extends Schema.TaggedErrorClass<Operation
},
) {}
export { MessageDecodeError } from "./session/error"
export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error"
export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
sessionID: SessionSchema.ID,

View File

@ -7,6 +7,7 @@ import { EventV2 } from "../event"
import { Location } from "../location"
import { SystemContext } from "../system-context"
import { SystemContextRegistry } from "../system-context-registry"
import { ContextSnapshotDecodeError } from "./error"
import { SessionEvent } from "./event"
import { SessionInput } from "./input"
import { SessionMessageID } from "./message-id"
@ -49,7 +50,7 @@ export function prepare(
context: SystemContextRegistry.Interface,
sessionID: SessionSchema.ID,
location: Location.Ref,
): Effect.Effect<Prepared, SystemContext.InitializationBlocked> {
): Effect.Effect<Prepared, SystemContext.InitializationBlocked | ContextSnapshotDecodeError> {
return retryRevisionMismatch(() => prepareOnce(db, events, context, sessionID, location)).pipe(
Effect.withSpan("SessionContextEpoch.prepare"),
)
@ -69,7 +70,9 @@ const prepareOnce = Effect.fnUntraced(function* (
return { baseline: generation.baseline, baselineSeq }
}
const snapshot = yield* Schema.decodeUnknownEffect(SystemContext.Snapshot)(stored.snapshot).pipe(Effect.orDie)
const snapshot = yield* Schema.decodeUnknownEffect(SystemContext.Snapshot)(stored.snapshot).pipe(
Effect.mapError((error) => new ContextSnapshotDecodeError({ sessionID, details: String(error) })),
)
const result =
stored.replacement_seq === null
? yield* SystemContext.reconcile(value, snapshot)

View File

@ -6,3 +6,15 @@ export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeErr
sessionID: SessionSchema.ID,
messageID: SessionMessage.ID,
}) {}
export class ContextSnapshotDecodeError extends Schema.TaggedErrorClass<ContextSnapshotDecodeError>()(
"Session.ContextSnapshotDecodeError",
{
sessionID: SessionSchema.ID,
details: Schema.String,
},
) {
override get message() {
return `Failed to decode context snapshot for session ${this.sessionID}: ${this.details}`
}
}

View File

@ -3,7 +3,7 @@ export * as SessionRunner from "./index"
import type { LLMError } from "@opencode-ai/llm"
import { Context, Effect, Schema } from "effect"
import { SessionSchema } from "../schema"
import type { MessageDecodeError } from "../error"
import type { ContextSnapshotDecodeError, MessageDecodeError } from "../error"
import { SessionRunnerModel } from "./model"
import type { SystemContext } from "../../system-context"
@ -19,6 +19,7 @@ export type RunError =
| LLMError
| SessionRunnerModel.Error
| MessageDecodeError
| ContextSnapshotDecodeError
| StepLimitExceededError
| SystemContext.InitializationBlocked

View File

@ -19,6 +19,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import { QuestionV2 } from "@opencode-ai/core/question"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { ContextSnapshotDecodeError } from "@opencode-ai/core/session/error"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionInput } from "@opencode-ai/core/session/input"
import { SessionMessage } from "@opencode-ai/core/session/message"
@ -631,6 +632,31 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("fails gracefully when a stored context snapshot cannot be decoded", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
response = []
yield* session.resume(sessionID)
yield* db
.update(SessionContextEpochTable)
.set({ snapshot: { invalid: { value: "bad" } } })
.where(eq(SessionContextEpochTable.session_id, sessionID))
.run()
.pipe(Effect.orDie)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
requests.length = 0
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(ContextSnapshotDecodeError)
expect(requests).toHaveLength(0)
}),
)
it.effect("does not create a source Location epoch after a concurrent Session move", () =>
Effect.gen(function* () {
yield* setup