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
|
|
@ -55,6 +55,7 @@ type Data = {
|
|||
family: Record<string, string[]>
|
||||
status: Record<string, DataSessionStatus>
|
||||
compaction: Partial<Record<string, string>>
|
||||
compactionReason: Partial<Record<string, "auto" | "manual">>
|
||||
message: Record<string, SessionMessage[]>
|
||||
input: Record<string, string[]>
|
||||
permission: Record<string, PermissionV2Request[]>
|
||||
|
|
@ -92,6 +93,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
family: {},
|
||||
status: {},
|
||||
compaction: {},
|
||||
compactionReason: {},
|
||||
message: {},
|
||||
input: {},
|
||||
permission: {},
|
||||
|
|
@ -145,6 +147,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
const item = messages.findLast((item) => item.type === "shell" && item.shell.id === shellID)
|
||||
return item?.type === "shell" ? item : undefined
|
||||
},
|
||||
compaction(messages: SessionMessage[]) {
|
||||
const item = messages.findLast(
|
||||
(item) => item.type === "compaction" && (item.status === "queued" || item.status === "running"),
|
||||
)
|
||||
return item?.type === "compaction" ? item : undefined
|
||||
},
|
||||
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantTool =>
|
||||
|
|
@ -580,8 +588,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
case "session.execution.started":
|
||||
setSessionStatus(event.data.sessionID, "running")
|
||||
break
|
||||
case "session.compaction.admitted":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
if (message.compaction(draft)) return
|
||||
message.append(draft, index, {
|
||||
id: event.data.inputID,
|
||||
type: "compaction",
|
||||
status: "queued",
|
||||
reason: "manual",
|
||||
summary: "",
|
||||
recent: "",
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.compaction.started":
|
||||
setStore("session", "compaction", event.data.sessionID, "")
|
||||
setStore("session", "compactionReason", event.data.sessionID, event.data.reason)
|
||||
if (event.data.reason === "manual")
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
const current = message.compaction(draft)
|
||||
if (current) current.status = "running"
|
||||
})
|
||||
break
|
||||
case "session.execution.succeeded":
|
||||
case "session.execution.failed":
|
||||
|
|
@ -589,6 +617,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
setSessionStatus(event.data.sessionID, "idle")
|
||||
if (store.session.compaction[event.data.sessionID] !== undefined)
|
||||
setStore("session", "compaction", event.data.sessionID, undefined)
|
||||
if (store.session.compactionReason[event.data.sessionID] !== undefined)
|
||||
setStore("session", "compactionReason", event.data.sessionID, undefined)
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
const currentAssistant = message.activeAssistant(draft)
|
||||
if (currentAssistant) currentAssistant.retry = undefined
|
||||
|
|
@ -619,13 +649,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.compaction.delta":
|
||||
setStore("session", "compaction", event.data.sessionID, (text) => (text ?? "") + event.data.text)
|
||||
if (store.session.compactionReason[event.data.sessionID] === "manual")
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
const current = message.compaction(draft)
|
||||
if (current) current.summary += event.data.text
|
||||
})
|
||||
break
|
||||
case "session.compaction.ended":
|
||||
setStore("session", "compaction", event.data.sessionID, undefined)
|
||||
setStore("session", "compactionReason", event.data.sessionID, undefined)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const current = event.data.reason === "manual" ? message.compaction(draft) : undefined
|
||||
if (current) {
|
||||
current.status = "completed"
|
||||
current.reason = event.data.reason
|
||||
current.summary = event.data.text
|
||||
current.recent = event.data.recent
|
||||
return
|
||||
}
|
||||
message.append(draft, index, {
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
|
|
@ -633,6 +678,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
})
|
||||
break
|
||||
case "session.compaction.failed":
|
||||
setStore("session", "compaction", event.data.sessionID, undefined)
|
||||
setStore("session", "compactionReason", event.data.sessionID, undefined)
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
const current = message.compaction(draft)
|
||||
if (current) current.status = "failed"
|
||||
})
|
||||
break
|
||||
case "permission.v2.asked":
|
||||
if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) break
|
||||
setStore("session", "permission", event.data.sessionID, [
|
||||
|
|
@ -785,6 +838,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
].toSorted((a, b) => a.time.created - b.time.created)
|
||||
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
|
||||
setStore("session", "message", sessionID, messages)
|
||||
const running = messages.find((message) => message.type === "compaction" && message.status === "running")
|
||||
setStore("session", "compaction", sessionID, running?.type === "compaction" ? running.summary : undefined)
|
||||
setStore(
|
||||
"session",
|
||||
"compactionReason",
|
||||
sessionID,
|
||||
running?.type === "compaction" ? running.reason : undefined,
|
||||
)
|
||||
},
|
||||
},
|
||||
permission: {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useProject } from "../../context/project"
|
|||
import { useData } from "../../context/data"
|
||||
import { SplitBorder } from "../../ui/border"
|
||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
|
||||
import { createSyntaxStyleMemo, generateSubtleSyntax, useTheme } from "../../context/theme"
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "../../component/prompt"
|
||||
|
|
@ -172,6 +172,16 @@ export function Session() {
|
|||
})
|
||||
onCleanup(() => setEpilogue())
|
||||
const messages = sessionMessages
|
||||
const transientCompaction = createMemo(() => {
|
||||
if (
|
||||
messages().some(
|
||||
(message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"),
|
||||
)
|
||||
)
|
||||
return
|
||||
const text = data.session.compaction(route.sessionID)
|
||||
return text === undefined ? undefined : { text }
|
||||
})
|
||||
const descendantSessionIDs = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return data.session.family(route.sessionID).filter((id) => id !== route.sessionID)
|
||||
|
|
@ -926,8 +936,8 @@ export function Session() {
|
|||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={data.session.compaction(route.sessionID)}>
|
||||
{(text) => <CompactionMessage text={text()} />}
|
||||
<Show when={transientCompaction()}>
|
||||
{(compaction) => <CompactionMessage status="running" text={compaction().text} />}
|
||||
</Show>
|
||||
<BackgroundToolHint messages={messages()} />
|
||||
<Show when={session()?.revert?.messageID}>
|
||||
|
|
@ -1100,7 +1110,7 @@ function SessionMessageView(props: { message: SessionMessage }) {
|
|||
</Show>
|
||||
</Match>
|
||||
<Match when={props.message.type === "compaction"}>
|
||||
<CompactionMessage />
|
||||
<CompactionMessage message={props.message as Extract<SessionMessage, { type: "compaction" }>} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
|
@ -1285,12 +1295,56 @@ function SessionSkillMessage(props: { message: Extract<SessionMessage, { type: "
|
|||
)
|
||||
}
|
||||
|
||||
function CompactionMessage(props: { text?: string }) {
|
||||
const { theme } = useTheme()
|
||||
function CompactionMessage(props: {
|
||||
message?: Extract<SessionMessage, { type: "compaction" }>
|
||||
status?: "running"
|
||||
text?: string
|
||||
}) {
|
||||
const ctx = use()
|
||||
const kv = useKV()
|
||||
const { theme, syntax } = useTheme()
|
||||
const status = () => props.message?.status ?? props.status
|
||||
const text = () => props.message?.summary ?? props.text ?? ""
|
||||
const color = () => (status() === "failed" ? theme.error : status() === "completed" ? theme.success : theme.textMuted)
|
||||
const border = () => (status() === "queued" ? theme.border : color())
|
||||
return (
|
||||
<box border={["top"]} title=" Compaction " titleAlignment="center" borderColor={theme.borderActive}>
|
||||
<Show when={props.text}>
|
||||
<text fg={theme.textMuted}>{props.text}</text>
|
||||
<box>
|
||||
<box flexDirection="row" alignItems="center">
|
||||
<box border={["top"]} borderColor={border()} flexGrow={1} />
|
||||
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
||||
<Switch>
|
||||
<Match when={status() === "running"}>
|
||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯</text>}>
|
||||
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={status() === "completed"}>
|
||||
<text fg={color()}>✓</text>
|
||||
</Match>
|
||||
<Match when={status() === "failed"}>
|
||||
<text fg={color()}>✗</text>
|
||||
</Match>
|
||||
<Match when={status() === "queued"}>
|
||||
<text fg={color()}>◇</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<text fg={color()}>{status() === "queued" ? "Compaction queued" : "Compaction"}</text>
|
||||
</box>
|
||||
<box border={["top"]} borderColor={border()} flexGrow={1} />
|
||||
</box>
|
||||
<Show when={text().trim()}>
|
||||
<box paddingTop={1} paddingLeft={3}>
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
streaming={status() === "running"}
|
||||
internalBlockMode="top-level"
|
||||
content={text().trim()}
|
||||
tableOptions={{ style: "grid" }}
|
||||
conceal={ctx.conceal()}
|
||||
fg={theme.markdownText}
|
||||
bg={theme.background}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,15 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
input: data.session.input.has(sessionID(), message.id),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
: message.type === "compaction"
|
||||
? [
|
||||
{
|
||||
id: message.id,
|
||||
created: message.time.created,
|
||||
input: message.status === "queued" || message.status === "running",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
() => setRows(reconcile(reduce())),
|
||||
),
|
||||
|
|
@ -93,9 +101,11 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
setRows(
|
||||
produce((draft) => {
|
||||
if (draft.some((row) => row.type === "message" && row.messageID === messageID)) return
|
||||
const queued = isQueued(messageID)
|
||||
const index = queued ? draft.length : queuedStart(draft)
|
||||
if (!queued) completePrevious(draft, index)
|
||||
const pending = isPending(messageID)
|
||||
const message = data.session.message.get(sessionID(), messageID)
|
||||
const index =
|
||||
message?.type === "compaction" && pending ? queuedStart(draft) : pending ? draft.length : queuedStart(draft)
|
||||
if (!pending) completePrevious(draft, index)
|
||||
draft.splice(index, 0, { type: "message", messageID })
|
||||
}),
|
||||
)
|
||||
|
|
@ -144,12 +154,14 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
}),
|
||||
)
|
||||
|
||||
const isQueued = (messageID: string) => {
|
||||
return data.session.input.has(sessionID(), messageID)
|
||||
const isPending = (messageID: string) => {
|
||||
const message = data.session.message.get(sessionID(), messageID)
|
||||
if (message?.type === "user") return data.session.input.has(sessionID(), messageID)
|
||||
return message?.type === "compaction" && (message.status === "queued" || message.status === "running")
|
||||
}
|
||||
|
||||
const queuedStart = (rows: SessionRow[]) => {
|
||||
const index = rows.findIndex((row) => row.type === "message" && isQueued(row.messageID))
|
||||
const index = rows.findIndex((row) => row.type === "message" && isPending(row.messageID))
|
||||
return index === -1 ? rows.length : index
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +173,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
}
|
||||
const subscriptions = [
|
||||
data.on("session.prompt.admitted", input),
|
||||
data.on("session.compaction.admitted", input),
|
||||
data.on("session.instructions.updated", message),
|
||||
data.on("session.synthetic", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.description?.trim())
|
||||
|
|
@ -169,7 +182,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
data.on("session.shell.started", message),
|
||||
data.on("session.agent.selected", message),
|
||||
data.on("session.model.selected", message),
|
||||
data.on("session.compaction.ended", message),
|
||||
data.on("session.compaction.ended", (event) => {
|
||||
if (event.data.reason !== "manual") message(event)
|
||||
}),
|
||||
data.on("session.text.delta", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` })
|
||||
|
|
@ -211,28 +226,33 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
|
||||
export function reduceSessionRows(messages: SessionMessage[], inputs = new Set<string>()) {
|
||||
const isInput = (message: SessionMessage) => inputs.has(message.id)
|
||||
return [...messages.filter((message) => !isInput(message)), ...messages.filter(isInput)].reduce<SessionRow[]>(
|
||||
(rows, message) => {
|
||||
if (message.type !== "assistant") {
|
||||
if (message.type === "synthetic" && !message.description?.trim()) return rows
|
||||
if (!isInput(message)) completePrevious(rows)
|
||||
rows.push({ type: "message", messageID: message.id })
|
||||
return rows
|
||||
}
|
||||
const ordinals = { text: 0, reasoning: 0 }
|
||||
message.content.forEach((part) => {
|
||||
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
|
||||
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
|
||||
append(rows, { messageID: message.id, partID }, part)
|
||||
})
|
||||
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) {
|
||||
completePrevious(rows)
|
||||
rows.push({ type: "assistant-footer", messageID: message.id })
|
||||
}
|
||||
return rows
|
||||
},
|
||||
[],
|
||||
const pendingCompactions = messages.filter(
|
||||
(message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"),
|
||||
)
|
||||
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
|
||||
return [
|
||||
...messages.filter((message) => !pending.has(message.id)),
|
||||
...pendingCompactions,
|
||||
...messages.filter(isInput),
|
||||
].reduce<SessionRow[]>((rows, message) => {
|
||||
if (message.type !== "assistant") {
|
||||
if (message.type === "synthetic" && !message.description?.trim()) return rows
|
||||
if (!pending.has(message.id)) completePrevious(rows)
|
||||
rows.push({ type: "message", messageID: message.id })
|
||||
return rows
|
||||
}
|
||||
const ordinals = { text: 0, reasoning: 0 }
|
||||
message.content.forEach((part) => {
|
||||
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
|
||||
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
|
||||
append(rows, { messageID: message.id, partID }, part)
|
||||
})
|
||||
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) {
|
||||
completePrevious(rows)
|
||||
rows.push({ type: "assistant-footer", messageID: message.id })
|
||||
}
|
||||
return rows
|
||||
}, [])
|
||||
}
|
||||
|
||||
export function resolvePart(message: SessionMessageAssistant, partID: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue