- #3124: Session tools now merge SDK and file-backed sessions for SQLite backend - #3125: Cache priming fixed for OpenCode >=1.3.14 empty workspace - #3127: Activity-based progress detection prevents infinite compaction on Kimi/Minimax All 29 new tests pass, 4885 total tests passing.
This commit is contained in:
@@ -16,6 +16,14 @@ import { acknowledgeCompactionGuard, isCompactionGuardActive } from "./compactio
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
import { startCountdown } from "./countdown"
|
||||
|
||||
function shouldAllowActivityProgress(modelID: string | undefined): boolean {
|
||||
if (!modelID) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !modelID.toLowerCase().includes("codex")
|
||||
}
|
||||
|
||||
export async function handleSessionIdle(args: {
|
||||
ctx: PluginInput
|
||||
sessionID: string
|
||||
@@ -182,7 +190,12 @@ export async function handleSessionIdle(args: {
|
||||
return
|
||||
}
|
||||
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, incompleteCount, todos)
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(
|
||||
sessionID,
|
||||
incompleteCount,
|
||||
todos,
|
||||
{ allowActivityProgress: shouldAllowActivityProgress(resolvedInfo?.model?.modelID) },
|
||||
)
|
||||
if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -38,6 +39,7 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -56,7 +58,10 @@ export function handleNonIdleEvent(args: {
|
||||
|
||||
if (targetSessionID) {
|
||||
const state = sessionStateStore.getExistingState(targetSessionID)
|
||||
if (state) state.abortDetectedAt = undefined
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
sessionStateStore.recordActivity(targetSessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(targetSessionID)
|
||||
}
|
||||
return
|
||||
@@ -69,6 +74,7 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
@@ -82,6 +88,7 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
|
||||
@@ -143,4 +143,56 @@ describe("createSessionStateStore", () => {
|
||||
expect(stagnatedAgainUpdate.hasProgressed).toBe(false)
|
||||
expect(stagnatedAgainUpdate.stagnationCount).toBe(1)
|
||||
})
|
||||
|
||||
test("given non-codex activity happens after a successful continuation, treats it as progress", () => {
|
||||
// given
|
||||
const sessionID = "ses-non-codex-activity-progress"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
const todos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
]
|
||||
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 1, todos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(
|
||||
sessionID,
|
||||
1,
|
||||
todos,
|
||||
{ allowActivityProgress: true },
|
||||
)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(true)
|
||||
expect(progressUpdate.progressSource).toBe("activity")
|
||||
expect(progressUpdate.stagnationCount).toBe(0)
|
||||
})
|
||||
|
||||
test("given codex activity happens after a successful continuation, keeps counting stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-codex-activity-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
const todos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
]
|
||||
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 1, todos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(
|
||||
sessionID,
|
||||
1,
|
||||
todos,
|
||||
{ allowActivityProgress: false },
|
||||
)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(false)
|
||||
expect(progressUpdate.progressSource).toBe("none")
|
||||
expect(progressUpdate.stagnationCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SessionState, Todo } from "./types"
|
||||
import type { ContinuationProgressOptions, SessionState, Todo } from "./types"
|
||||
|
||||
type TimerHandle = number | { unref?: () => void }
|
||||
|
||||
@@ -16,6 +16,8 @@ interface TrackedSessionState {
|
||||
lastAccessedAt: number
|
||||
lastCompletedCount?: number
|
||||
lastTodoSnapshot?: string
|
||||
activitySignalCount: number
|
||||
lastObservedActivitySignalCount?: number
|
||||
}
|
||||
|
||||
export interface ContinuationProgressUpdate {
|
||||
@@ -23,13 +25,19 @@ export interface ContinuationProgressUpdate {
|
||||
previousStagnationCount: number
|
||||
stagnationCount: number
|
||||
hasProgressed: boolean
|
||||
progressSource: "none" | "todo"
|
||||
progressSource: "none" | "todo" | "activity"
|
||||
}
|
||||
|
||||
export interface SessionStateStore {
|
||||
getState: (sessionID: string) => SessionState
|
||||
getExistingState: (sessionID: string) => SessionState | undefined
|
||||
trackContinuationProgress: (sessionID: string, incompleteCount: number, todos?: Todo[]) => ContinuationProgressUpdate
|
||||
recordActivity: (sessionID: string) => void
|
||||
trackContinuationProgress: (
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[],
|
||||
options?: ContinuationProgressOptions,
|
||||
) => ContinuationProgressUpdate
|
||||
resetContinuationProgress: (sessionID: string) => void
|
||||
cancelCountdown: (sessionID: string) => void
|
||||
cleanup: (sessionID: string) => void
|
||||
@@ -96,6 +104,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
const trackedSession: TrackedSessionState = {
|
||||
state: rawState,
|
||||
lastAccessedAt: Date.now(),
|
||||
activitySignalCount: 0,
|
||||
}
|
||||
sessions.set(sessionID, trackedSession)
|
||||
return trackedSession
|
||||
@@ -114,10 +123,16 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function recordActivity(sessionID: string): void {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
trackedSession.activitySignalCount += 1
|
||||
}
|
||||
|
||||
function trackContinuationProgress(
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[]
|
||||
todos?: Todo[],
|
||||
options: ContinuationProgressOptions = {},
|
||||
): ContinuationProgressUpdate {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
const state = trackedSession.state
|
||||
@@ -125,6 +140,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
const previousStagnationCount = state.stagnationCount
|
||||
const currentCompletedCount = todos?.filter((todo) => todo.status === "completed").length
|
||||
const currentTodoSnapshot = todos ? getTodoSnapshot(todos) : undefined
|
||||
const currentActivitySignalCount = trackedSession.activitySignalCount
|
||||
const hasCompletedMoreTodos =
|
||||
currentCompletedCount !== undefined
|
||||
&& trackedSession.lastCompletedCount !== undefined
|
||||
@@ -133,6 +149,10 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
currentTodoSnapshot !== undefined
|
||||
&& trackedSession.lastTodoSnapshot !== undefined
|
||||
&& currentTodoSnapshot !== trackedSession.lastTodoSnapshot
|
||||
const hasObservedExternalActivity =
|
||||
options.allowActivityProgress === true
|
||||
&& trackedSession.lastObservedActivitySignalCount !== undefined
|
||||
&& currentActivitySignalCount > trackedSession.lastObservedActivitySignalCount
|
||||
const hadSuccessfulInjectionAwaitingProgressCheck = state.awaitingPostInjectionProgressCheck === true
|
||||
|
||||
state.lastIncompleteCount = incompleteCount
|
||||
@@ -142,6 +162,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
if (currentTodoSnapshot !== undefined) {
|
||||
trackedSession.lastTodoSnapshot = currentTodoSnapshot
|
||||
}
|
||||
trackedSession.lastObservedActivitySignalCount = currentActivitySignalCount
|
||||
|
||||
if (previousIncompleteCount === undefined) {
|
||||
state.stagnationCount = 0
|
||||
@@ -156,7 +177,9 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
|
||||
const progressSource = incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoSnapshotChanged
|
||||
? "todo"
|
||||
: "none"
|
||||
: hasObservedExternalActivity
|
||||
? "activity"
|
||||
: "none"
|
||||
|
||||
if (progressSource !== "none") {
|
||||
state.stagnationCount = 0
|
||||
@@ -204,6 +227,8 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
trackedSession.lastCompletedCount = undefined
|
||||
trackedSession.lastTodoSnapshot = undefined
|
||||
trackedSession.activitySignalCount = 0
|
||||
trackedSession.lastObservedActivitySignalCount = undefined
|
||||
}
|
||||
|
||||
function cancelCountdown(sessionID: string): void {
|
||||
@@ -247,6 +272,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
return {
|
||||
getState,
|
||||
getExistingState,
|
||||
recordActivity,
|
||||
trackContinuationProgress,
|
||||
resetContinuationProgress,
|
||||
cancelCountdown,
|
||||
|
||||
@@ -65,3 +65,7 @@ export interface ResolveLatestMessageInfoResult {
|
||||
resolvedInfo?: ResolvedMessageInfo
|
||||
encounteredCompaction: boolean
|
||||
}
|
||||
|
||||
export interface ContinuationProgressOptions {
|
||||
allowActivityProgress?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user