feat(core): add durable compaction barrier (#35371)
This commit is contained in:
parent
bd947658bb
commit
04b673432c
39 changed files with 1486 additions and 396 deletions
|
|
@ -33,7 +33,6 @@ import { MessageDecodeError } from "./session/error"
|
|||
import { SessionEvent } from "./session/event"
|
||||
import { SessionInput } from "./session/input"
|
||||
import { Snapshot } from "./snapshot"
|
||||
import { SessionCompaction } from "./session/compaction"
|
||||
import { SessionRevert } from "./session/revert"
|
||||
import { Revert } from "@opencode-ai/schema/revert"
|
||||
import { FSUtil } from "./fs-util"
|
||||
|
|
@ -96,6 +95,7 @@ type CreateInput = CreateBaseInput &
|
|||
({ location: Location.Ref; parentID?: never } | { parentID: SessionSchema.ID; location?: never })
|
||||
|
||||
type CompactInput = {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +125,13 @@ export class AttachmentError extends Schema.TaggedErrorClass<AttachmentError>()(
|
|||
uri: Schema.String,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
export class CompactionConflictError extends Schema.TaggedErrorClass<CompactionConflictError>()(
|
||||
"Session.CompactionConflictError",
|
||||
{
|
||||
sessionID: SessionSchema.ID,
|
||||
inputID: SessionMessage.ID,
|
||||
},
|
||||
) {}
|
||||
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
}) {}
|
||||
|
|
@ -140,6 +147,7 @@ export type Error =
|
|||
| OperationUnavailableError
|
||||
| PromptConflictError
|
||||
| AttachmentError
|
||||
| CompactionConflictError
|
||||
| BusyError
|
||||
| SkillNotFoundError
|
||||
| CommandV2.NotFoundError
|
||||
|
|
@ -224,7 +232,7 @@ export interface Interface {
|
|||
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
|
||||
readonly compact: (
|
||||
input: CompactInput,
|
||||
) => Effect.Effect<void, NotFoundError | BusyError | MessageDecodeError | OperationUnavailableError>
|
||||
) => Effect.Effect<SessionInput.Compaction, NotFoundError | CompactionConflictError>
|
||||
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
|
||||
readonly background: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
|
|
@ -626,19 +634,20 @@ const layer = Layer.effect(
|
|||
})
|
||||
}),
|
||||
compact: Effect.fn("V2Session.compact")(function* (input) {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
// TODO: admit manual compaction as durable pending work, like prompt input, instead of rejecting active sessions.
|
||||
if ((yield* execution.active).has(input.sessionID)) return yield* new BusyError({ sessionID: input.sessionID })
|
||||
const context = yield* store.context(input.sessionID)
|
||||
const compacted = yield* Effect.gen(function* () {
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
return yield* compaction.compactManual({ session, messages: context })
|
||||
yield* result.get(input.sessionID)
|
||||
const inputID = input.id ?? SessionMessage.ID.create()
|
||||
const admitted = yield* SessionInput.admitCompaction(db, events, {
|
||||
id: inputID,
|
||||
sessionID: input.sessionID,
|
||||
}).pipe(
|
||||
Effect.provide(locations.get(session.location)),
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
? new CompactionConflictError({ sessionID: input.sessionID, inputID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (!compacted) return yield* new OperationUnavailableError({ operation: "compact" })
|
||||
return undefined
|
||||
yield* execution.wake(input.sessionID)
|
||||
return admitted
|
||||
}),
|
||||
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
|
||||
yield* result.get(sessionID)
|
||||
|
|
@ -734,11 +743,7 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf
|
|||
const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* (input: PromptInput.Prompt) {
|
||||
const fs = yield* FSUtil.Service
|
||||
const files = input.files
|
||||
? yield* Effect.forEach(
|
||||
input.files,
|
||||
(file) => materializeAttachment(fs, file),
|
||||
{ concurrency: 8 },
|
||||
)
|
||||
? yield* Effect.forEach(input.files, (file) => materializeAttachment(fs, file), { concurrency: 8 })
|
||||
: undefined
|
||||
return Prompt.make({ text: input.text, agents: input.agents, files })
|
||||
})
|
||||
|
|
@ -769,7 +774,11 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct
|
|||
const content =
|
||||
mime === "text/plain" && resolved.start !== undefined
|
||||
? Buffer.from(
|
||||
Buffer.from(resolved.bytes).toString("utf8").split("\n").slice(resolved.start - 1, resolved.end).join("\n"),
|
||||
Buffer.from(resolved.bytes)
|
||||
.toString("utf8")
|
||||
.split("\n")
|
||||
.slice(resolved.start - 1, resolved.end)
|
||||
.join("\n"),
|
||||
)
|
||||
: resolved.bytes
|
||||
return FileAttachment.create({
|
||||
|
|
@ -799,13 +808,13 @@ const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (
|
|||
},
|
||||
catch: () => new AttachmentError({ uri, message: `Invalid file URI: ${uri}` }),
|
||||
})
|
||||
const info = yield* fs.stat(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
const info = yield* fs
|
||||
.stat(target)
|
||||
.pipe(Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })))
|
||||
if (info.type === "Directory") {
|
||||
const entries = yield* fs.readDirectoryEntries(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
const entries = yield* fs
|
||||
.readDirectoryEntries(target)
|
||||
.pipe(Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })))
|
||||
return {
|
||||
bytes: Buffer.from(
|
||||
entries
|
||||
|
|
@ -827,9 +836,9 @@ const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (
|
|||
uri,
|
||||
message: `Attachment exceeds the ${MAX_ATTACHMENT_BYTES} byte limit: ${uri}`,
|
||||
})
|
||||
const bytes = yield* fs.readFile(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
const bytes = yield* fs
|
||||
.readFile(target)
|
||||
.pipe(Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })))
|
||||
return { bytes, source: { type: "uri" as const, uri }, start, end, name: path.basename(target), mime: undefined }
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue