refactor(session): simplify pending withdrawal
This commit is contained in:
parent
330ab9acae
commit
d7ffc7fec1
12 changed files with 143 additions and 130 deletions
|
|
@ -31,7 +31,7 @@ export function createV2SessionReducer() {
|
|||
return result([...source])
|
||||
case "session.input.withdrawn":
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
return result([...source])
|
||||
return
|
||||
case "session.input.promoted": {
|
||||
const input = pending.get(key(sessionID, event.data.inputID))
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
|
|
|
|||
|
|
@ -249,44 +249,38 @@ export type Endpoint5_21Input = { readonly sessionID: Session.ID }
|
|||
export type Endpoint5_21Output = ReadonlyArray<SessionPending.Info>
|
||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
|
||||
|
||||
export type Endpoint5_22Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_22Output = boolean
|
||||
export type SessionPendingWithdrawOperation<E = never> = (
|
||||
export type Endpoint5_22Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_22Output = ReadonlyArray<InstructionEntry.Info>
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint5_22Input,
|
||||
) => Effect.Effect<Endpoint5_22Output, E>
|
||||
|
||||
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_23Output = ReadonlyArray<InstructionEntry.Info>
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint5_23Input,
|
||||
) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = {
|
||||
export type Endpoint5_23Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly key: InstructionEntry.Key
|
||||
readonly value: Schema.Json
|
||||
}
|
||||
export type Endpoint5_24Output = void
|
||||
export type Endpoint5_23Output = void
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint5_23Input,
|
||||
) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
||||
export type Endpoint5_24Output = void
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint5_24Input,
|
||||
) => Effect.Effect<Endpoint5_24Output, E>
|
||||
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
||||
export type Endpoint5_25Output = void
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint5_25Input,
|
||||
) => Effect.Effect<Endpoint5_25Output, E>
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
||||
export type Endpoint5_25Output = { readonly text: string }
|
||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
||||
export type Endpoint5_26Output = { readonly text: string }
|
||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = {
|
||||
export type Endpoint5_26Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly after?: Event.Seq | undefined
|
||||
readonly follow?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_27Output =
|
||||
export type Endpoint5_26Output =
|
||||
| (
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
|
|
@ -842,19 +836,25 @@ export type Endpoint5_27Output =
|
|||
}
|
||||
)
|
||||
| EventLog.Synced
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_27Input) => Stream.Stream<Endpoint5_27Output, E>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_26Input) => Stream.Stream<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_27Output = void
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||
|
||||
export type Endpoint5_28Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_28Output = void
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
|
||||
export type Endpoint5_29Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_29Output = void
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||
export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
||||
export type Endpoint5_29Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||
|
||||
export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
||||
export type Endpoint5_30Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
|
||||
export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_30Output = boolean
|
||||
export type SessionPendingWithdrawOperation<E = never> = (
|
||||
input: Endpoint5_30Input,
|
||||
) => Effect.Effect<Endpoint5_30Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21I
|
|||
|
||||
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
|
||||
preserveEffect<Endpoint5_22Output>()(
|
||||
raw["session.pending.withdraw"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
|
|
@ -500,37 +500,29 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
|
|||
|
||||
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
||||
preserveEffect<Endpoint5_23Output>()(
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
raw["session.instructions.entry.put"]({
|
||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||
payload: { value: input["value"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveEffect<Endpoint5_26Output>()(
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
preserveStream<Endpoint5_27Output>()(
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveStream<Endpoint5_26Output>()(
|
||||
Stream.unwrap(
|
||||
raw["session.log"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
|
|
@ -542,19 +534,27 @@ const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27I
|
|||
),
|
||||
)
|
||||
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
preserveEffect<Endpoint5_27Output>()(
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
||||
preserveEffect<Endpoint5_28Output>()(
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
||||
preserveEffect<Endpoint5_29Output>()(
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
|
||||
preserveEffect<Endpoint5_30Output>()(
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
raw["session.pending.withdraw"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
|
|
@ -580,13 +580,13 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
|||
wait: Endpoint5_16(raw),
|
||||
revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) },
|
||||
context: Endpoint5_20(raw),
|
||||
pending: { list: Endpoint5_21(raw), withdraw: Endpoint5_22(raw) },
|
||||
instructions: { entry: { list: Endpoint5_23(raw), put: Endpoint5_24(raw), remove: Endpoint5_25(raw) } },
|
||||
generate: Endpoint5_26(raw),
|
||||
log: Endpoint5_27(raw),
|
||||
interrupt: Endpoint5_28(raw),
|
||||
background: Endpoint5_29(raw),
|
||||
message: Endpoint5_30(raw),
|
||||
pending: { list: Endpoint5_21(raw), withdraw: Endpoint5_30(raw) },
|
||||
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
||||
generate: Endpoint5_25(raw),
|
||||
log: Endpoint5_26(raw),
|
||||
interrupt: Endpoint5_27(raw),
|
||||
background: Endpoint5_28(raw),
|
||||
message: Endpoint5_29(raw),
|
||||
})
|
||||
|
||||
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ import type {
|
|||
SessionContextOutput,
|
||||
SessionPendingListInput,
|
||||
SessionPendingListOutput,
|
||||
SessionPendingWithdrawInput,
|
||||
SessionPendingWithdrawOutput,
|
||||
SessionInstructionsEntryListInput,
|
||||
SessionInstructionsEntryListOutput,
|
||||
SessionInstructionsEntryPutInput,
|
||||
|
|
@ -72,6 +70,8 @@ import type {
|
|||
SessionBackgroundOutput,
|
||||
SessionMessageInput,
|
||||
SessionMessageOutput,
|
||||
SessionPendingWithdrawInput,
|
||||
SessionPendingWithdrawOutput,
|
||||
MessageListInput,
|
||||
MessageListOutput,
|
||||
ModelListInput,
|
||||
|
|
|
|||
|
|
@ -3170,13 +3170,6 @@ export type SessionPendingListInput = { readonly sessionID: { readonly sessionID
|
|||
|
||||
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
|
||||
|
||||
export type SessionPendingWithdrawInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
}
|
||||
|
||||
export type SessionPendingWithdrawOutput = { data: boolean }["data"]
|
||||
|
||||
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionInstructionsEntryListOutput = { data: Array<InstructionEntryInfo> }["data"]
|
||||
|
|
@ -3226,6 +3219,13 @@ export type SessionMessageInput = {
|
|||
|
||||
export type SessionMessageOutput = { data: SessionMessageInfo }["data"]
|
||||
|
||||
export type SessionPendingWithdrawInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
}
|
||||
|
||||
export type SessionPendingWithdrawOutput = { data: boolean }["data"]
|
||||
|
||||
export type MessageListInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly limit?: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as SessionPending from "./pending"
|
||||
|
||||
import { and, asc, eq, or } from "drizzle-orm"
|
||||
import { and, asc, eq, or, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import {
|
||||
Compaction,
|
||||
|
|
@ -113,20 +113,24 @@ const admittedFromHistory = Effect.fn("SessionPending.admittedFromHistory")(func
|
|||
sessionID: SessionSchema.ID,
|
||||
id: SessionMessage.ID,
|
||||
) {
|
||||
const rows = yield* db
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, admittedEventType)))
|
||||
.all()
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, sessionID),
|
||||
eq(EventTable.type, admittedEventType),
|
||||
sql`json_extract(${EventTable.data}, '$.inputID') = ${id}`,
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
for (const row of rows) {
|
||||
if (row) {
|
||||
const decoded = decodeAdmittedEvent(row.data)
|
||||
if (decoded._tag !== "Some" || decoded.value.inputID !== id) continue
|
||||
const base = {
|
||||
id,
|
||||
sessionID,
|
||||
timeCreated: DateTime.makeUnsafe(row.created),
|
||||
}
|
||||
if (decoded._tag !== "Some" || decoded.value.inputID !== id)
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
const base = { id, sessionID, timeCreated: DateTime.makeUnsafe(row.created) }
|
||||
return decoded.value.input.type === "user"
|
||||
? User.make({ ...base, ...decoded.value.input })
|
||||
: Synthetic.make({ ...base, ...decoded.value.input })
|
||||
|
|
@ -164,16 +168,22 @@ const wasWithdrawn = Effect.fn("SessionPending.wasWithdrawn")(function* (
|
|||
sessionID: SessionSchema.ID,
|
||||
id: SessionMessage.ID,
|
||||
) {
|
||||
const rows = yield* db
|
||||
const row = yield* db
|
||||
.select({ data: EventTable.data })
|
||||
.from(EventTable)
|
||||
.where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, withdrawnEventType)))
|
||||
.all()
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, sessionID),
|
||||
eq(EventTable.type, withdrawnEventType),
|
||||
sql`json_extract(${EventTable.data}, '$.inputID') = ${id}`,
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.some((row) => {
|
||||
const decoded = decodeWithdrawnEvent(row.data)
|
||||
return decoded._tag === "Some" && decoded.value.inputID === id
|
||||
})
|
||||
if (!row) return false
|
||||
const decoded = decodeWithdrawnEvent(row.data)
|
||||
return decoded._tag === "Some" && decoded.value.inputID === id
|
||||
})
|
||||
|
||||
export const admit = Effect.fn("SessionPending.admit")(function* (
|
||||
|
|
@ -341,14 +351,13 @@ export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompac
|
|||
* message insert inside the same event transaction; the deleted row is what
|
||||
* makes the table pending-only.
|
||||
*/
|
||||
export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* (
|
||||
const consumeInput = Effect.fn("SessionPending.consumeInput")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
},
|
||||
) {
|
||||
if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
|
||||
|
|
@ -361,6 +370,17 @@ export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(funct
|
|||
return stored
|
||||
})
|
||||
|
||||
export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
},
|
||||
) {
|
||||
if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return yield* consumeInput(db, input)
|
||||
})
|
||||
|
||||
export const projectWithdrawn = Effect.fn("SessionPending.projectWithdrawn")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
|
|
@ -368,16 +388,7 @@ export const projectWithdrawn = Effect.fn("SessionPending.projectWithdrawn")(fun
|
|||
readonly sessionID: SessionSchema.ID
|
||||
},
|
||||
) {
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = fromRow(deleted)
|
||||
if (stored.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
return yield* consumeInput(db, input)
|
||||
})
|
||||
|
||||
export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(function* (
|
||||
|
|
|
|||
|
|
@ -494,22 +494,6 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.pending.withdraw", "/api/session/:sessionID/pending/:inputID/withdraw", {
|
||||
params: { sessionID: Session.ID, inputID: SessionMessage.ID },
|
||||
success: Schema.Struct({ data: Schema.Boolean }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.pending.withdraw",
|
||||
summary: "Withdraw pending session input",
|
||||
description:
|
||||
"Withdraw one admitted input before promotion. Returns true when the input was withdrawn or had already been withdrawn, and false when it is no longer pending so callers can fall back to reverting projected history.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.instructions.entry.list", "/api/session/:sessionID/instructions/entries", {
|
||||
params: { sessionID: Session.ID },
|
||||
|
|
@ -644,6 +628,22 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.pending.withdraw", "/api/session/:sessionID/pending/:inputID/withdraw", {
|
||||
params: { sessionID: Session.ID, inputID: SessionMessage.ID },
|
||||
success: Schema.Struct({ data: Schema.Boolean }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.pending.withdraw",
|
||||
summary: "Withdraw pending session input",
|
||||
description:
|
||||
"Withdraw one admitted input before promotion. Returns true when the input was withdrawn or had already been withdrawn, and false when it is no longer pending so callers can fall back to reverting projected history.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "session",
|
||||
|
|
|
|||
|
|
@ -188,6 +188,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
index.set(item.id, messages.length)
|
||||
messages.push(item)
|
||||
},
|
||||
reindex(messages: SessionMessageInfo[], index: Map<string, number>, start: number) {
|
||||
for (let position = start; position < messages.length; position++) {
|
||||
const item = messages[position]
|
||||
if (item) index.set(item.id, position)
|
||||
}
|
||||
},
|
||||
activeAssistant(messages: SessionMessageInfo[]) {
|
||||
const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed)
|
||||
return item?.type === "assistant" ? item : undefined
|
||||
|
|
@ -395,8 +401,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
existing.time.created = event.created
|
||||
draft.splice(position, 1)
|
||||
draft.push(existing)
|
||||
index.clear()
|
||||
draft.forEach((message, indexValue) => index.set(message.id, indexValue))
|
||||
message.reindex(draft, index, position)
|
||||
})
|
||||
setStore(
|
||||
"session",
|
||||
|
|
@ -445,15 +450,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
const position = index.get(event.data.inputID)
|
||||
if (position === undefined) return
|
||||
draft.splice(position, 1)
|
||||
index.clear()
|
||||
draft.forEach((message, indexValue) => index.set(message.id, indexValue))
|
||||
index.delete(event.data.inputID)
|
||||
message.reindex(draft, index, position)
|
||||
})
|
||||
setStore(
|
||||
"session",
|
||||
"input",
|
||||
event.data.sessionID,
|
||||
(store.session.input[event.data.sessionID] ?? []).filter((id) => id !== event.data.inputID),
|
||||
)
|
||||
if (store.session.input[event.data.sessionID]?.includes(event.data.inputID))
|
||||
setStore(
|
||||
"session",
|
||||
"input",
|
||||
event.data.sessionID,
|
||||
(store.session.input[event.data.sessionID] ?? []).filter((id) => id !== event.data.inputID),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "session.instructions.updated":
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ export function DialogMessage(props: {
|
|||
void undoMessage(client.api, {
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
pending: data.session.input.has(props.sessionID, props.messageID),
|
||||
}).catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
||||
dialog.clear()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -594,7 +594,6 @@ export function Session() {
|
|||
void undoMessage(client.api, {
|
||||
sessionID: route.sessionID,
|
||||
messageID: message.id,
|
||||
pending: data.session.input.has(route.sessionID, message.id),
|
||||
}).catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
||||
prompt()?.set({
|
||||
...projectedPromptInput(message),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import type { OpenCodeClient } from "@opencode-ai/client"
|
|||
|
||||
export function undoMessage(
|
||||
client: OpenCodeClient,
|
||||
input: { readonly sessionID: string; readonly messageID: string; readonly pending: boolean },
|
||||
input: { readonly sessionID: string; readonly messageID: string },
|
||||
) {
|
||||
const revert = () => client.session.revert.stage(input).then(() => undefined)
|
||||
if (!input.pending) return revert()
|
||||
return client.session.pending
|
||||
.withdraw({ sessionID: input.sessionID, inputID: input.messageID })
|
||||
.then((withdrawn) => (withdrawn ? undefined : revert()))
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import { OpenCode } from "@opencode-ai/client"
|
|||
import { undoMessage } from "../../../src/routes/session/undo"
|
||||
|
||||
test.each([
|
||||
{ pending: true, withdrawn: true, expected: ["withdraw"] },
|
||||
{ pending: true, withdrawn: false, expected: ["withdraw", "revert"] },
|
||||
{ pending: false, withdrawn: false, expected: ["revert"] },
|
||||
])("routes undo for pending=$pending withdrawn=$withdrawn", async ({ pending, withdrawn, expected }) => {
|
||||
{ withdrawn: true, expected: ["withdraw"] },
|
||||
{ withdrawn: false, expected: ["withdraw", "revert"] },
|
||||
])("routes undo for withdrawn=$withdrawn", async ({ withdrawn, expected }) => {
|
||||
const calls: string[] = []
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
|
|
@ -21,7 +20,7 @@ test.each([
|
|||
),
|
||||
})
|
||||
|
||||
await undoMessage(client, { sessionID: "ses_test", messageID: "msg_user", pending })
|
||||
await undoMessage(client, { sessionID: "ses_test", messageID: "msg_user" })
|
||||
|
||||
expect(calls).toEqual([...expected])
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue