b2497f1327
- background_output: snapshot read cursor before consuming, restore on /undo message removal so re-reads return data (fixes #2915) - MCP loader: preserve oauth field in transformMcpServer, add scope/ projectPath filtering so local-scoped MCPs only load in matching directories (fixes #2917) - runtime-fallback: add 'reached your usage limit' to retryable error patterns so quota exhaustion triggers model fallback (fixes #2918) Verified: bun test (4606 pass / 0 fail), tsc --noEmit clean
109 lines
2.5 KiB
TypeScript
109 lines
2.5 KiB
TypeScript
type MessageTime =
|
|
| { created?: number | string }
|
|
| number
|
|
| string
|
|
| undefined
|
|
|
|
type MessageInfo = {
|
|
id?: string
|
|
time?: MessageTime
|
|
}
|
|
|
|
export type CursorMessage = {
|
|
info?: MessageInfo
|
|
}
|
|
|
|
export interface CursorState {
|
|
lastKey?: string
|
|
lastCount: number
|
|
}
|
|
|
|
const sessionCursors = new Map<string, CursorState>()
|
|
|
|
function cloneCursorState(state: CursorState | undefined): CursorState | undefined {
|
|
if (!state) return undefined
|
|
return {
|
|
lastKey: state.lastKey,
|
|
lastCount: state.lastCount,
|
|
}
|
|
}
|
|
|
|
function buildMessageKey(message: CursorMessage, index: number): string {
|
|
const id = message.info?.id
|
|
if (id) return `id:${id}`
|
|
|
|
const time = message.info?.time
|
|
if (typeof time === "number" || typeof time === "string") {
|
|
return `t:${time}:${index}`
|
|
}
|
|
|
|
const created = time?.created
|
|
if (typeof created === "number") {
|
|
return `t:${created}:${index}`
|
|
}
|
|
if (typeof created === "string") {
|
|
return `t:${created}:${index}`
|
|
}
|
|
|
|
return `i:${index}`
|
|
}
|
|
|
|
export function consumeNewMessages<T extends CursorMessage>(
|
|
sessionID: string | undefined,
|
|
messages: T[]
|
|
): T[] {
|
|
if (!sessionID) return messages
|
|
|
|
const keys = messages.map((message, index) => buildMessageKey(message, index))
|
|
const cursor = sessionCursors.get(sessionID)
|
|
let startIndex = 0
|
|
|
|
if (cursor) {
|
|
if (cursor.lastCount > messages.length) {
|
|
startIndex = 0
|
|
} else if (cursor.lastKey) {
|
|
const lastIndex = keys.lastIndexOf(cursor.lastKey)
|
|
if (lastIndex >= 0) {
|
|
startIndex = lastIndex + 1
|
|
} else {
|
|
// History changed without a shrink; reset to avoid skipping messages.
|
|
startIndex = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.length === 0) {
|
|
sessionCursors.delete(sessionID)
|
|
} else {
|
|
sessionCursors.set(sessionID, {
|
|
lastKey: keys[keys.length - 1],
|
|
lastCount: messages.length,
|
|
})
|
|
}
|
|
|
|
return messages.slice(startIndex)
|
|
}
|
|
|
|
export function resetMessageCursor(sessionID?: string): void {
|
|
if (sessionID) {
|
|
sessionCursors.delete(sessionID)
|
|
return
|
|
}
|
|
sessionCursors.clear()
|
|
}
|
|
|
|
export function getMessageCursor(sessionID: string | undefined): CursorState | undefined {
|
|
if (!sessionID) return undefined
|
|
return cloneCursorState(sessionCursors.get(sessionID))
|
|
}
|
|
|
|
export function restoreMessageCursor(sessionID: string | undefined, cursor: CursorState | undefined): void {
|
|
if (!sessionID) return
|
|
if (!cursor) {
|
|
sessionCursors.delete(sessionID)
|
|
return
|
|
}
|
|
|
|
sessionCursors.set(sessionID, cloneCursorState(cursor)!)
|
|
}
|