Merge pull request #3319 from EZotoff/fix/remove-activity-stagnation-bypass
This commit is contained in:
@@ -28,7 +28,6 @@ function createStateStore(): {
|
||||
getState: () => state,
|
||||
getExistingState: () => state,
|
||||
startPruneInterval: () => {},
|
||||
recordActivity: () => {},
|
||||
trackContinuationProgress: () => progressUpdate,
|
||||
resetContinuationProgress: (sessionID: string) => {
|
||||
resetCalls.push(sessionID)
|
||||
|
||||
@@ -2,27 +2,19 @@ import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { normalizeSDKResponse } from "../../shared"
|
||||
import { log } from "../../shared/logger"
|
||||
import { getAgentConfigKey } from "../../shared/agent-display-names"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
import { ABORT_WINDOW_MS, CONTINUATION_COOLDOWN_MS, DEFAULT_SKIP_AGENTS, FAILURE_RESET_WINDOW_MS, HOOK_NAME, MAX_CONSECUTIVE_FAILURES } from "./constants"
|
||||
import { isLastAssistantMessageAborted } from "./abort-detection"
|
||||
import { acknowledgeCompactionGuard, isCompactionGuardActive } from "./compaction-guard"
|
||||
import { ABORT_WINDOW_MS, CONTINUATION_COOLDOWN_MS, DEFAULT_SKIP_AGENTS, FAILURE_RESET_WINDOW_MS, HOOK_NAME, MAX_CONSECUTIVE_FAILURES } from "./constants"
|
||||
import { startCountdown } from "./countdown"
|
||||
import { hasUnansweredQuestion } from "./pending-question-detection"
|
||||
import { resolveLatestMessageInfo } from "./resolve-message-info"
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
import { shouldStopForStagnation } from "./stagnation-detection"
|
||||
import { getIncompleteCount } from "./todo"
|
||||
import type { MessageInfo, MessageWithInfo, ResolvedMessageInfo, Todo } from "./types"
|
||||
import { resolveLatestMessageInfo } from "./resolve-message-info"
|
||||
import { acknowledgeCompactionGuard, isCompactionGuardActive } from "./compaction-guard"
|
||||
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")
|
||||
}
|
||||
import type { MessageWithInfo, ResolvedMessageInfo, Todo } from "./types"
|
||||
|
||||
export async function handleSessionIdle(args: {
|
||||
ctx: PluginInput
|
||||
@@ -140,7 +132,7 @@ export async function handleSessionIdle(args: {
|
||||
}
|
||||
|
||||
const effectiveCooldown =
|
||||
CONTINUATION_COOLDOWN_MS * Math.pow(2, Math.min(state.consecutiveFailures, 5))
|
||||
CONTINUATION_COOLDOWN_MS * 2 ** Math.min(state.consecutiveFailures, 5)
|
||||
if (state.lastInjectedAt && Date.now() - state.lastInjectedAt < effectiveCooldown) {
|
||||
log(`[${HOOK_NAME}] Skipped: cooldown active`, { sessionID, effectiveCooldown, consecutiveFailures: state.consecutiveFailures })
|
||||
return
|
||||
@@ -204,7 +196,6 @@ export async function handleSessionIdle(args: {
|
||||
sessionID,
|
||||
incompleteCount,
|
||||
todos,
|
||||
{ allowActivityProgress: shouldAllowActivityProgress(resolvedInfo?.model?.modelID) },
|
||||
)
|
||||
if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
|
||||
return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { log } from "../../shared/logger"
|
||||
import { resolveMessageEventSessionID, resolveSessionEventID } from "../../shared/event-session-id"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
import { COUNTDOWN_GRACE_PERIOD_MS, HOOK_NAME } from "./constants"
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
@@ -30,7 +30,6 @@ export function handleNonIdleEvent(args: {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
state.tokenLimitDetected = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -41,7 +40,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -57,7 +55,6 @@ export function handleNonIdleEvent(args: {
|
||||
const state = sessionStateStore.getExistingState(targetSessionID)
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
sessionStateStore.recordActivity(targetSessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(targetSessionID)
|
||||
}
|
||||
@@ -71,7 +68,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
@@ -85,7 +81,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ describe("createSessionStateStore", () => {
|
||||
expect(stagnatedAgainUpdate.stagnationCount).toBe(1)
|
||||
})
|
||||
|
||||
test("given non-codex activity happens after a successful continuation, treats it as progress", () => {
|
||||
test("given no todo changes after a successful continuation, keeps counting stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-non-codex-activity-progress"
|
||||
const sessionID = "ses-no-todo-change-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
const todos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
@@ -154,40 +154,12 @@ describe("createSessionStateStore", () => {
|
||||
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ContinuationProgressOptions, SessionState, Todo } from "./types"
|
||||
import type { SessionState, Todo } from "./types"
|
||||
|
||||
type TimerHandle = number | { unref?: () => void }
|
||||
|
||||
@@ -16,8 +16,6 @@ interface TrackedSessionState {
|
||||
lastAccessedAt: number
|
||||
lastCompletedCount?: number
|
||||
lastTodoSnapshot?: string
|
||||
activitySignalCount: number
|
||||
lastObservedActivitySignalCount?: number
|
||||
}
|
||||
|
||||
export interface ContinuationProgressUpdate {
|
||||
@@ -25,19 +23,17 @@ export interface ContinuationProgressUpdate {
|
||||
previousStagnationCount: number
|
||||
stagnationCount: number
|
||||
hasProgressed: boolean
|
||||
progressSource: "none" | "todo" | "activity"
|
||||
progressSource: "none" | "todo"
|
||||
}
|
||||
|
||||
export interface SessionStateStore {
|
||||
getState: (sessionID: string) => SessionState
|
||||
getExistingState: (sessionID: string) => SessionState | undefined
|
||||
startPruneInterval: () => void
|
||||
recordActivity: (sessionID: string) => void
|
||||
trackContinuationProgress: (
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[],
|
||||
options?: ContinuationProgressOptions,
|
||||
) => ContinuationProgressUpdate
|
||||
resetContinuationProgress: (sessionID: string) => void
|
||||
cancelCountdown: (sessionID: string) => void
|
||||
@@ -113,7 +109,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
const trackedSession: TrackedSessionState = {
|
||||
state: rawState,
|
||||
lastAccessedAt: Date.now(),
|
||||
activitySignalCount: 0,
|
||||
}
|
||||
sessions.set(sessionID, trackedSession)
|
||||
return trackedSession
|
||||
@@ -132,16 +127,10 @@ 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[],
|
||||
options: ContinuationProgressOptions = {},
|
||||
): ContinuationProgressUpdate {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
const state = trackedSession.state
|
||||
@@ -149,7 +138,6 @@ 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
|
||||
@@ -158,10 +146,6 @@ 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
|
||||
@@ -171,7 +155,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
if (currentTodoSnapshot !== undefined) {
|
||||
trackedSession.lastTodoSnapshot = currentTodoSnapshot
|
||||
}
|
||||
trackedSession.lastObservedActivitySignalCount = currentActivitySignalCount
|
||||
|
||||
if (previousIncompleteCount === undefined) {
|
||||
state.stagnationCount = 0
|
||||
@@ -184,13 +167,9 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
const progressSource = incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoSnapshotChanged
|
||||
? "todo"
|
||||
: hasObservedExternalActivity
|
||||
? "activity"
|
||||
: "none"
|
||||
const hasProgressed = incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoSnapshotChanged
|
||||
|
||||
if (progressSource !== "none") {
|
||||
if (hasProgressed) {
|
||||
state.stagnationCount = 0
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
return {
|
||||
@@ -198,7 +177,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
previousStagnationCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: true,
|
||||
progressSource,
|
||||
progressSource: "todo",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +215,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
trackedSession.lastCompletedCount = undefined
|
||||
trackedSession.lastTodoSnapshot = undefined
|
||||
trackedSession.activitySignalCount = 0
|
||||
trackedSession.lastObservedActivitySignalCount = undefined
|
||||
}
|
||||
|
||||
function cancelCountdown(sessionID: string): void {
|
||||
@@ -282,7 +259,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
getState,
|
||||
getExistingState,
|
||||
startPruneInterval,
|
||||
recordActivity,
|
||||
trackContinuationProgress,
|
||||
resetContinuationProgress,
|
||||
cancelCountdown,
|
||||
|
||||
@@ -68,7 +68,3 @@ export interface ResolveLatestMessageInfoResult {
|
||||
encounteredCompaction: boolean
|
||||
latestMessageWasCompaction: boolean
|
||||
}
|
||||
|
||||
export interface ContinuationProgressOptions {
|
||||
allowActivityProgress?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user