Fix session status hydration and compaction transitions
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
|||||||
getSessions,
|
getSessions,
|
||||||
setActiveSession,
|
setActiveSession,
|
||||||
} from "../../stores/sessions"
|
} from "../../stores/sessions"
|
||||||
import { setSessionCompactionState } from "../../stores/session-compaction"
|
|
||||||
import { showAlertDialog } from "../../stores/alerts"
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
import type { Instance } from "../../types/instance"
|
import type { Instance } from "../../types/instance"
|
||||||
import type { MessageRecord } from "../../stores/message-v2/types"
|
import type { MessageRecord } from "../../stores/message-v2/types"
|
||||||
@@ -241,7 +240,6 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSessionCompactionState(instance.id, sessionId, true)
|
|
||||||
await requestData(
|
await requestData(
|
||||||
instance.client.session.summarize({
|
instance.client.session.summarize({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
@@ -251,7 +249,6 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
"session.summarize",
|
"session.summarize",
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSessionCompactionState(instance.id, sessionId, false)
|
|
||||||
log.error("Failed to compact session", error)
|
log.error("Failed to compact session", error)
|
||||||
const message = error instanceof Error ? error.message : "Failed to compact session"
|
const message = error instanceof Error ? error.message : "Failed to compact session"
|
||||||
showAlertDialog(`Compact failed: ${message}`, {
|
showAlertDialog(`Compact failed: ${message}`, {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Session } from "../types/session"
|
import { mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||||
import type { Message } from "../types/message"
|
import type { Message } from "../types/message"
|
||||||
|
|
||||||
import { instances } from "./instances"
|
import { instances } from "./instances"
|
||||||
import { preferences, setAgentModelPreference } from "./preferences"
|
import { preferences, setAgentModelPreference } from "./preferences"
|
||||||
import { setSessionCompactionState } from "./session-compaction"
|
|
||||||
import {
|
import {
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
agents,
|
agents,
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
setProviders,
|
setProviders,
|
||||||
setSessionInfoByInstance,
|
setSessionInfoByInstance,
|
||||||
setSessions,
|
setSessions,
|
||||||
setSessionStatus,
|
|
||||||
sessions,
|
sessions,
|
||||||
loading,
|
loading,
|
||||||
setLoading,
|
setLoading,
|
||||||
@@ -29,7 +27,6 @@ import {
|
|||||||
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
|
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
|
||||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { deriveSessionStatusFromMessages } from "./session-status"
|
|
||||||
import { seedSessionMessagesV2, reconcilePendingPermissionsV2 } from "./message-v2/bridge"
|
import { seedSessionMessagesV2, reconcilePendingPermissionsV2 } from "./message-v2/bridge"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
import { clearCacheForSession } from "../lib/global-cache"
|
import { clearCacheForSession } from "../lib/global-cache"
|
||||||
@@ -81,15 +78,31 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let statusById: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
const statusResponse = await instance.client.session.status()
|
||||||
|
if (statusResponse.data && typeof statusResponse.data === "object") {
|
||||||
|
statusById = statusResponse.data as Record<string, any>
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to fetch session status:", error)
|
||||||
|
}
|
||||||
|
|
||||||
const existingSessions = sessions().get(instanceId)
|
const existingSessions = sessions().get(instanceId)
|
||||||
|
|
||||||
for (const apiSession of response.data) {
|
for (const apiSession of response.data) {
|
||||||
const existingSession = existingSessions?.get(apiSession.id)
|
const existingSession = existingSessions?.get(apiSession.id)
|
||||||
|
|
||||||
const compactingFlag = (apiSession.time as (Session["time"] & { compacting?: number | boolean }) | undefined)?.compacting
|
|
||||||
const isCompacting = typeof compactingFlag === "number" ? compactingFlag > 0 : Boolean(compactingFlag)
|
|
||||||
const existingStatus = existingSession?.status
|
const existingStatus = existingSession?.status
|
||||||
|
|
||||||
|
let status: SessionStatus
|
||||||
|
if (existingStatus === "compacting") {
|
||||||
|
status = "compacting"
|
||||||
|
} else {
|
||||||
|
const rawStatus = (apiSession as any)?.status ?? statusById[apiSession.id]
|
||||||
|
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||||
|
status = hasType ? mapSdkSessionStatus(rawStatus) : existingStatus ?? "idle"
|
||||||
|
}
|
||||||
|
|
||||||
sessionMap.set(apiSession.id, {
|
sessionMap.set(apiSession.id, {
|
||||||
id: apiSession.id,
|
id: apiSession.id,
|
||||||
instanceId,
|
instanceId,
|
||||||
@@ -97,7 +110,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
parentId: apiSession.parentID || null,
|
parentId: apiSession.parentID || null,
|
||||||
agent: existingSession?.agent ?? "",
|
agent: existingSession?.agent ?? "",
|
||||||
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
||||||
status: isCompacting ? "compacting" : (existingStatus ?? "idle"),
|
status,
|
||||||
version: apiSession.version,
|
version: apiSession.version,
|
||||||
time: {
|
time: {
|
||||||
...apiSession.time,
|
...apiSession.time,
|
||||||
@@ -138,11 +151,6 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const session of sessionMap.values()) {
|
|
||||||
const flag = (session.time as (Session["time"] & { compacting?: number | boolean }) | undefined)?.compacting
|
|
||||||
const active = typeof flag === "number" ? flag > 0 : Boolean(flag)
|
|
||||||
setSessionCompactionState(instanceId, session.id, active)
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
|
pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -380,7 +388,6 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
|||||||
|
|
||||||
syncInstanceSessionIndicator(instanceId)
|
syncInstanceSessionIndicator(instanceId)
|
||||||
|
|
||||||
setSessionCompactionState(instanceId, sessionId, false)
|
|
||||||
clearSessionDraftPrompt(instanceId, sessionId)
|
clearSessionDraftPrompt(instanceId, sessionId)
|
||||||
|
|
||||||
// Drop normalized message state and caches for this session
|
// Drop normalized message state and caches for this session
|
||||||
@@ -537,7 +544,20 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
"session.messages",
|
"session.messages",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!Array.isArray(apiMessages) || apiMessages.length === 0) {
|
if (!Array.isArray(apiMessages)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat empty sessions as loaded to avoid re-fetch loops.
|
||||||
|
setMessagesLoaded((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
const loadedSet = next.get(instanceId) || new Set()
|
||||||
|
loadedSet.add(sessionId)
|
||||||
|
next.set(instanceId, loadedSet)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
if (apiMessages.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,11 +650,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
// After message hydration, try to attach any pending permissions to tool-call part ids.
|
// After message hydration, try to attach any pending permissions to tool-call part ids.
|
||||||
reconcilePendingPermissionsV2(instanceId, sessionId)
|
reconcilePendingPermissionsV2(instanceId, sessionId)
|
||||||
|
|
||||||
if (!alreadyLoaded) {
|
|
||||||
const nextStatus = deriveSessionStatusFromMessages(instanceId, sessionId)
|
|
||||||
setSessionStatus(instanceId, sessionId, nextStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to load messages:", error)
|
log.error("Failed to load messages:", error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ import type { PermissionReplyEventPropertiesLike, PermissionRequestLike } from "
|
|||||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||||
import { instances, addPermissionToQueue, removePermissionFromQueue } from "./instances"
|
import { instances, addPermissionToQueue, removePermissionFromQueue } from "./instances"
|
||||||
import { showAlertDialog } from "./alerts"
|
import { showAlertDialog } from "./alerts"
|
||||||
import { createClientSession, Session, SessionStatus } from "../types/session"
|
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||||
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
|
|
||||||
import { loadMessages } from "./session-api"
|
import { loadMessages } from "./session-api"
|
||||||
import { setSessionCompactionState } from "./session-compaction"
|
|
||||||
import {
|
import {
|
||||||
applyPartUpdateV2,
|
applyPartUpdateV2,
|
||||||
replaceMessageIdV2,
|
replaceMessageIdV2,
|
||||||
@@ -56,31 +55,16 @@ interface TuiToastEvent {
|
|||||||
|
|
||||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||||
|
|
||||||
const mapSdkSessionStatus = (status: EventSessionStatus["properties"]["status"]): SessionStatus => {
|
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||||
if (!status || status.type === "idle") {
|
|
||||||
return "idle"
|
|
||||||
}
|
|
||||||
if (status.type === "retry") {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySessionStatus(
|
|
||||||
instanceId: string,
|
|
||||||
sessionId: string,
|
|
||||||
status: SessionStatus,
|
|
||||||
bumpUpdatedOnTransition = false,
|
|
||||||
) {
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
const current = session.status ?? "idle"
|
const current = session.status ?? "idle"
|
||||||
if (current === status) return false
|
if (current === status) return false
|
||||||
|
|
||||||
session.status = status
|
if (current === "compacting" && status !== "compacting") {
|
||||||
|
return false
|
||||||
if (bumpUpdatedOnTransition) {
|
|
||||||
session.time = { ...(session.time ?? {}), updated: Date.now() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.status = status
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +78,17 @@ async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<
|
|||||||
"session.get",
|
"session.get",
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetched = createClientSession(info, instanceId)
|
let fetchedStatus: SessionStatus = "idle"
|
||||||
|
try {
|
||||||
|
const statuses = await requestData<Record<string, any>>(instance.client.session.status(), "session.status")
|
||||||
|
const rawStatus = (info as any)?.status ?? statuses?.[sessionId]
|
||||||
|
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||||
|
fetchedStatus = hasType ? mapSdkSessionStatus(rawStatus) : "idle"
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to fetch session status", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||||
|
|
||||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||||
|
|
||||||
@@ -106,7 +100,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<
|
|||||||
...fetched,
|
...fetched,
|
||||||
agent: existing?.agent ?? fetched.agent,
|
agent: existing?.agent ?? fetched.agent,
|
||||||
model: existing?.model ?? fetched.model,
|
model: existing?.model ?? fetched.model,
|
||||||
status: existing?.status ?? fetched.status,
|
status: existing?.status === "compacting" ? "compacting" : fetched.status,
|
||||||
pendingPermission: existing?.pendingPermission ?? fetched.pendingPermission,
|
pendingPermission: existing?.pendingPermission ?? fetched.pendingPermission,
|
||||||
}
|
}
|
||||||
instanceSessions.set(sessionId, merged)
|
instanceSessions.set(sessionId, merged)
|
||||||
@@ -124,19 +118,14 @@ async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureSessionStatus(
|
function ensureSessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||||
instanceId: string,
|
|
||||||
sessionId: string,
|
|
||||||
status: SessionStatus,
|
|
||||||
bumpUpdatedOnTransition = false,
|
|
||||||
) {
|
|
||||||
const instanceSessions = sessions().get(instanceId)
|
const instanceSessions = sessions().get(instanceId)
|
||||||
const existing = instanceSessions?.get(sessionId)
|
const existing = instanceSessions?.get(sessionId)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if ((existing.status ?? "idle") === status) {
|
if ((existing.status ?? "idle") === status) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
applySessionStatus(instanceId, sessionId, status, bumpUpdatedOnTransition)
|
applySessionStatus(instanceId, sessionId, status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +137,7 @@ function ensureSessionStatus(
|
|||||||
const pending = (async () => {
|
const pending = (async () => {
|
||||||
const fetched = await fetchSessionInfo(instanceId, sessionId)
|
const fetched = await fetchSessionInfo(instanceId, sessionId)
|
||||||
if (!fetched) return
|
if (!fetched) return
|
||||||
applySessionStatus(instanceId, sessionId, status, bumpUpdatedOnTransition)
|
applySessionStatus(instanceId, sessionId, status)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
pendingSessionFetches.set(key, pending)
|
pendingSessionFetches.set(key, pending)
|
||||||
@@ -193,17 +182,10 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
const sessionId = typeof part.sessionID === "string" ? part.sessionID : fallbackSessionId
|
const sessionId = typeof part.sessionID === "string" ? part.sessionID : fallbackSessionId
|
||||||
const messageId = typeof part.messageID === "string" ? part.messageID : fallbackMessageId
|
const messageId = typeof part.messageID === "string" ? part.messageID : fallbackMessageId
|
||||||
if (!sessionId || !messageId) return
|
if (!sessionId || !messageId) return
|
||||||
const session = instanceSessions?.get(sessionId)
|
if (part.type === "compaction") {
|
||||||
if (!session) {
|
ensureSessionStatus(instanceId, sessionId, "compacting")
|
||||||
ensureSessionStatus(instanceId, sessionId, "working", true)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== "working" && session.status !== "compacting") {
|
|
||||||
applySessionStatus(instanceId, sessionId, "working", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
const store = messageStoreBus.getOrCreate(instanceId)
|
||||||
const role: MessageRole = resolveMessageRole(messageInfo)
|
const role: MessageRole = resolveMessageRole(messageInfo)
|
||||||
const createdAt = typeof messageInfo?.time?.created === "number" ? messageInfo.time.created : Date.now()
|
const createdAt = typeof messageInfo?.time?.created === "number" ? messageInfo.time.created : Date.now()
|
||||||
@@ -246,15 +228,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
const messageId = typeof info.id === "string" ? info.id : undefined
|
const messageId = typeof info.id === "string" ? info.id : undefined
|
||||||
if (!sessionId || !messageId) return
|
if (!sessionId || !messageId) return
|
||||||
|
|
||||||
const session = instanceSessions?.get(sessionId)
|
withSession(instanceId, sessionId, (session) => {
|
||||||
if (!session) {
|
session.time = { ...(session.time ?? {}), updated: Date.now() }
|
||||||
ensureSessionStatus(instanceId, sessionId, "working", true)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.status !== "working" && session.status !== "compacting") {
|
|
||||||
applySessionStatus(instanceId, sessionId, "working", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
const store = messageStoreBus.getOrCreate(instanceId)
|
||||||
|
|
||||||
@@ -295,10 +271,6 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||||||
|
|
||||||
if (!info) return
|
if (!info) return
|
||||||
|
|
||||||
const compactingFlag = info.time?.compacting
|
|
||||||
const isCompacting = typeof compactingFlag === "number" ? compactingFlag > 0 : Boolean(compactingFlag)
|
|
||||||
setSessionCompactionState(instanceId, info.id, isCompacting)
|
|
||||||
|
|
||||||
const instanceSessions = sessions().get(instanceId) ?? new Map<string, Session>()
|
const instanceSessions = sessions().get(instanceId) ?? new Map<string, Session>()
|
||||||
|
|
||||||
const existingSession = instanceSessions.get(info.id)
|
const existingSession = instanceSessions.get(info.id)
|
||||||
@@ -314,7 +286,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||||||
providerId: "",
|
providerId: "",
|
||||||
modelId: "",
|
modelId: "",
|
||||||
},
|
},
|
||||||
status: isCompacting ? "compacting" : "idle",
|
status: "idle",
|
||||||
version: info.version || "0",
|
version: info.version || "0",
|
||||||
time: info.time
|
time: info.time
|
||||||
? { ...info.time }
|
? { ...info.time }
|
||||||
@@ -344,14 +316,10 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||||||
...existingSession.time,
|
...existingSession.time,
|
||||||
...(info.time ?? {}),
|
...(info.time ?? {}),
|
||||||
}
|
}
|
||||||
if (!info.time?.updated) {
|
|
||||||
mergedTime.updated = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedSession = {
|
const updatedSession = {
|
||||||
...existingSession,
|
...existingSession,
|
||||||
title: info.title || existingSession.title,
|
title: info.title || existingSession.title,
|
||||||
status: isCompacting ? "compacting" : (existingSession.status ?? "idle"),
|
status: existingSession.status ?? "idle",
|
||||||
time: mergedTime,
|
time: mergedTime,
|
||||||
revert: info.revert
|
revert: info.revert
|
||||||
? {
|
? {
|
||||||
@@ -383,7 +351,7 @@ function handleSessionIdle(instanceId: string, event: EventSessionIdle): void {
|
|||||||
const sessionId = event.properties?.sessionID
|
const sessionId = event.properties?.sessionID
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
ensureSessionStatus(instanceId, sessionId, "idle", true)
|
ensureSessionStatus(instanceId, sessionId, "idle")
|
||||||
log.info(`[SSE] Session idle: ${sessionId}`)
|
log.info(`[SSE] Session idle: ${sessionId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +360,7 @@ function handleSessionStatus(instanceId: string, event: EventSessionStatus): voi
|
|||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
const status = mapSdkSessionStatus(event.properties.status)
|
const status = mapSdkSessionStatus(event.properties.status)
|
||||||
ensureSessionStatus(instanceId, sessionId, status, true)
|
ensureSessionStatus(instanceId, sessionId, status)
|
||||||
log.info(`[SSE] Session status updated: ${sessionId}`, { status })
|
log.info(`[SSE] Session status updated: ${sessionId}`, { status })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,14 +370,14 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted
|
|||||||
|
|
||||||
log.info(`[SSE] Session compacted: ${sessionID}`)
|
log.info(`[SSE] Session compacted: ${sessionID}`)
|
||||||
|
|
||||||
setSessionCompactionState(instanceId, sessionID, false)
|
const existing = sessions().get(instanceId)?.get(sessionID)
|
||||||
ensureSessionStatus(instanceId, sessionID, "idle", true)
|
if (existing) {
|
||||||
|
withSession(instanceId, sessionID, (session) => {
|
||||||
withSession(instanceId, sessionID, (session) => {
|
session.status = "working"
|
||||||
const time = { ...(session.time ?? {}) }
|
})
|
||||||
time.compacting = 0
|
} else {
|
||||||
session.time = time
|
ensureSessionStatus(instanceId, sessionID, "working")
|
||||||
})
|
}
|
||||||
|
|
||||||
loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload session after compaction", error))
|
loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload session after compaction", error))
|
||||||
|
|
||||||
|
|||||||
@@ -290,36 +290,6 @@ function withSession(instanceId: string, sessionId: string, updater: (session: S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSessionCompactionState(instanceId: string, sessionId: string, isCompacting: boolean): void {
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
|
||||||
const time = { ...(session.time ?? {}) } as Session["time"] & { compacting?: number | boolean; updated?: number }
|
|
||||||
const compactingFlag = time.compacting
|
|
||||||
const wasCompacting = typeof compactingFlag === "number" ? compactingFlag > 0 : Boolean(compactingFlag)
|
|
||||||
|
|
||||||
const shouldAlreadyBeCompacting = isCompacting
|
|
||||||
const isAlreadyCorrect =
|
|
||||||
wasCompacting === isCompacting &&
|
|
||||||
(shouldAlreadyBeCompacting ? session.status === "compacting" : session.status !== "compacting")
|
|
||||||
|
|
||||||
if (isAlreadyCorrect) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wasCompacting !== isCompacting) {
|
|
||||||
time.compacting = isCompacting ? Date.now() : 0
|
|
||||||
time.updated = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
session.time = time
|
|
||||||
|
|
||||||
if (isCompacting) {
|
|
||||||
session.status = "compacting"
|
|
||||||
} else if (session.status === "compacting") {
|
|
||||||
session.status = "idle"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSessionPendingPermission(instanceId: string, sessionId: string, pending: boolean): void {
|
function setSessionPendingPermission(instanceId: string, sessionId: string, pending: boolean): void {
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
if (session.pendingPermission === pending) return false
|
if (session.pendingPermission === pending) return false
|
||||||
@@ -547,7 +517,6 @@ export {
|
|||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
pruneDraftPrompts,
|
pruneDraftPrompts,
|
||||||
withSession,
|
withSession,
|
||||||
setSessionCompactionState,
|
|
||||||
setSessionPendingPermission,
|
setSessionPendingPermission,
|
||||||
setSessionStatus,
|
setSessionStatus,
|
||||||
setActiveSession,
|
setActiveSession,
|
||||||
|
|||||||
@@ -1,173 +1,17 @@
|
|||||||
import type { Session, SessionStatus } from "../types/session"
|
import type { Session, SessionStatus } from "../types/session"
|
||||||
import type { MessageInfo } from "../types/message"
|
|
||||||
import type { MessageRecord } from "./message-v2/types"
|
|
||||||
import { getInstanceSessionIndicatorStatusCached, sessions } from "./session-state"
|
import { getInstanceSessionIndicatorStatusCached, sessions } from "./session-state"
|
||||||
import { isSessionCompactionActive } from "./session-compaction"
|
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
|
||||||
|
|
||||||
function getSession(instanceId: string, sessionId: string): Session | null {
|
function getSession(instanceId: string, sessionId: string): Session | null {
|
||||||
const instanceSessions = sessions().get(instanceId)
|
const instanceSessions = sessions().get(instanceId)
|
||||||
return instanceSessions?.get(sessionId) ?? null
|
return instanceSessions?.get(sessionId) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSessionCompacting(session: Session): boolean {
|
|
||||||
const time = (session.time as (Session["time"] & { compacting?: number }) | undefined)
|
|
||||||
const compactingFlag = time?.compacting
|
|
||||||
if (typeof compactingFlag === "number") {
|
|
||||||
return compactingFlag > 0
|
|
||||||
}
|
|
||||||
return Boolean(compactingFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestInfoFromStore(instanceId: string, sessionId: string, role?: MessageInfo["role"]): MessageInfo | undefined {
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
|
||||||
const messageIds = store.getSessionMessageIds(sessionId)
|
|
||||||
let latest: MessageInfo | undefined
|
|
||||||
let latestTimestamp = Number.NEGATIVE_INFINITY
|
|
||||||
for (const id of messageIds) {
|
|
||||||
const info = store.getMessageInfo(id)
|
|
||||||
if (!info) continue
|
|
||||||
if (role && info.role !== role) continue
|
|
||||||
const timestamp = info.time?.created ?? 0
|
|
||||||
if (timestamp >= latestTimestamp) {
|
|
||||||
latest = info
|
|
||||||
latestTimestamp = timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return latest
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastMessageFromStore(instanceId: string, sessionId: string): MessageRecord | undefined {
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
|
||||||
const messageIds = store.getSessionMessageIds(sessionId)
|
|
||||||
let latest: MessageRecord | undefined
|
|
||||||
let latestTimestamp = Number.NEGATIVE_INFINITY
|
|
||||||
for (const id of messageIds) {
|
|
||||||
const record = store.getMessage(id)
|
|
||||||
if (!record) continue
|
|
||||||
const info = store.getMessageInfo(id)
|
|
||||||
const timestamp = info?.time?.created ?? record.createdAt ?? Number.NEGATIVE_INFINITY
|
|
||||||
if (timestamp >= latestTimestamp) {
|
|
||||||
latest = record
|
|
||||||
latestTimestamp = timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return latest
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getInfoCreatedTimestamp(info?: MessageInfo): number {
|
|
||||||
if (!info) {
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
}
|
|
||||||
const created = info.time?.created
|
|
||||||
if (typeof created === "number" && Number.isFinite(created)) {
|
|
||||||
return created
|
|
||||||
}
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAssistantCompletionTimestamp(info?: MessageInfo): number {
|
|
||||||
if (!info) {
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
}
|
|
||||||
const completed = (info.time as { completed?: number } | undefined)?.completed
|
|
||||||
if (typeof completed === "number" && Number.isFinite(completed)) {
|
|
||||||
return completed
|
|
||||||
}
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAssistantInfoPending(info?: MessageInfo): boolean {
|
|
||||||
if (!info) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const completed = (info.time as { completed?: number } | undefined)?.completed
|
|
||||||
if (completed === undefined || completed === null) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const created = getInfoCreatedTimestamp(info)
|
|
||||||
return completed < created
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAssistantStillGeneratingRecord(record: MessageRecord, info?: MessageInfo): boolean {
|
|
||||||
if (record.role !== "assistant") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.status === "error") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.status === "streaming" || record.status === "sending") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const completedAt = (info?.time as { completed?: number } | undefined)?.completed
|
|
||||||
if (completedAt !== undefined && completedAt !== null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(record.status === "complete" || record.status === "sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function deriveSessionStatusFromMessages(instanceId: string, sessionId: string): SessionStatus {
|
|
||||||
const session = getSession(instanceId, sessionId)
|
|
||||||
if (!session) {
|
|
||||||
return "idle"
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
|
||||||
|
|
||||||
if (isSessionCompactionActive(instanceId, sessionId) || isSessionCompacting(session)) {
|
|
||||||
return "compacting"
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestUserInfo = getLatestInfoFromStore(instanceId, sessionId, "user")
|
|
||||||
const latestAssistantInfo = getLatestInfoFromStore(instanceId, sessionId, "assistant")
|
|
||||||
|
|
||||||
const lastRecord = getLastMessageFromStore(instanceId, sessionId)
|
|
||||||
|
|
||||||
if (!lastRecord) {
|
|
||||||
const latestInfo = latestUserInfo ?? latestAssistantInfo
|
|
||||||
if (!latestInfo) {
|
|
||||||
return "idle"
|
|
||||||
}
|
|
||||||
if (latestInfo.role === "user") {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
const infoCompleted = latestInfo.time?.completed
|
|
||||||
return infoCompleted ? "idle" : "working"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastRecord.role === "user") {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
const infoForRecord = store.getMessageInfo(lastRecord.id) ?? latestAssistantInfo
|
|
||||||
if (infoForRecord && isAssistantStillGeneratingRecord(lastRecord, infoForRecord)) {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAssistantInfoPending(latestAssistantInfo)) {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
|
|
||||||
const userTimestamp = getInfoCreatedTimestamp(latestUserInfo)
|
|
||||||
const assistantCompletedAt = getAssistantCompletionTimestamp(latestAssistantInfo)
|
|
||||||
if (userTimestamp > assistantCompletedAt) {
|
|
||||||
return "working"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "idle"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSessionStatus(instanceId: string, sessionId: string): SessionStatus {
|
export function getSessionStatus(instanceId: string, sessionId: string): SessionStatus {
|
||||||
const session = getSession(instanceId, sessionId)
|
const session = getSession(instanceId, sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return "idle"
|
return "idle"
|
||||||
}
|
}
|
||||||
return session.status ?? deriveSessionStatusFromMessages(instanceId, sessionId)
|
return session.status ?? "idle"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
Provider as SDKProvider,
|
Provider as SDKProvider,
|
||||||
Model as SDKModel,
|
Model as SDKModel,
|
||||||
} from "@opencode-ai/sdk"
|
} from "@opencode-ai/sdk"
|
||||||
|
import type { SessionStatus as SDKSessionStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
// Export SDK types for external use
|
// Export SDK types for external use
|
||||||
export type {
|
export type {
|
||||||
@@ -15,6 +16,15 @@ export type {
|
|||||||
|
|
||||||
export type SessionStatus = "idle" | "working" | "compacting"
|
export type SessionStatus = "idle" | "working" | "compacting"
|
||||||
|
|
||||||
|
export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined): SessionStatus {
|
||||||
|
if (!status || status.type === "idle") {
|
||||||
|
return "idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
// "busy" and "retry" both mean there's active work.
|
||||||
|
return "working"
|
||||||
|
}
|
||||||
|
|
||||||
// Our client-specific Session interface extending SDK Session
|
// Our client-specific Session interface extending SDK Session
|
||||||
export interface Session
|
export interface Session
|
||||||
extends Omit<import("@opencode-ai/sdk").Session, "projectID" | "directory" | "parentID"> {
|
extends Omit<import("@opencode-ai/sdk").Session, "projectID" | "directory" | "parentID"> {
|
||||||
@@ -36,6 +46,7 @@ export function createClientSession(
|
|||||||
instanceId: string,
|
instanceId: string,
|
||||||
agent: string = "",
|
agent: string = "",
|
||||||
model: { providerId: string; modelId: string } = { providerId: "", modelId: "" },
|
model: { providerId: string; modelId: string } = { providerId: "", modelId: "" },
|
||||||
|
status: SessionStatus = "idle",
|
||||||
): Session {
|
): Session {
|
||||||
return {
|
return {
|
||||||
...sdkSession,
|
...sdkSession,
|
||||||
@@ -43,7 +54,7 @@ export function createClientSession(
|
|||||||
parentId: sdkSession.parentID || null,
|
parentId: sdkSession.parentID || null,
|
||||||
agent,
|
agent,
|
||||||
model,
|
model,
|
||||||
status: "idle",
|
status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user