Merge pull request #2108 from code-yeongyu/fix/issue-2100-reset-strategy-race-condition
fix(ralph-loop): fix race condition in --strategy=reset
This commit is contained in:
commit
31eb7f5d28
@ -33,15 +33,6 @@ export async function continueIteration(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundState = options.loopState.setSessionID(newSessionID)
|
|
||||||
if (!boundState) {
|
|
||||||
log(`[${HOOK_NAME}] Failed to bind loop state to new session`, {
|
|
||||||
previousSessionID: options.previousSessionID,
|
|
||||||
newSessionID,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await injectContinuationPrompt(ctx, {
|
await injectContinuationPrompt(ctx, {
|
||||||
sessionID: newSessionID,
|
sessionID: newSessionID,
|
||||||
inheritFromSessionID: options.previousSessionID,
|
inheritFromSessionID: options.previousSessionID,
|
||||||
@ -51,6 +42,16 @@ export async function continueIteration(
|
|||||||
})
|
})
|
||||||
|
|
||||||
await selectSessionInTui(ctx.client, newSessionID)
|
await selectSessionInTui(ctx.client, newSessionID)
|
||||||
|
|
||||||
|
const boundState = options.loopState.setSessionID(newSessionID)
|
||||||
|
if (!boundState) {
|
||||||
|
log(`[${HOOK_NAME}] Failed to bind loop state to new session`, {
|
||||||
|
previousSessionID: options.previousSessionID,
|
||||||
|
newSessionID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
113
src/hooks/ralph-loop/reset-strategy-race-condition.test.ts
Normal file
113
src/hooks/ralph-loop/reset-strategy-race-condition.test.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createRalphLoopHook } from "./index"
|
||||||
|
|
||||||
|
function createDeferred(): {
|
||||||
|
promise: Promise<void>
|
||||||
|
resolve: () => void
|
||||||
|
} {
|
||||||
|
let resolvePromise: (() => void) | null = null
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
|
resolvePromise = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise,
|
||||||
|
resolve: () => {
|
||||||
|
if (resolvePromise) {
|
||||||
|
resolvePromise()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntil(condition: () => boolean): Promise<void> {
|
||||||
|
for (let index = 0; index < 100; index++) {
|
||||||
|
if (condition()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Condition was not met in time")
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ralph-loop reset strategy race condition", () => {
|
||||||
|
test("should continue iteration when old session idle arrives before TUI switch completes", async () => {
|
||||||
|
// given - reset strategy loop with blocked TUI session switch
|
||||||
|
const promptCalls: Array<{ sessionID: string; text: string }> = []
|
||||||
|
const createSessionCalls: Array<{ parentID?: string }> = []
|
||||||
|
let selectSessionCalls = 0
|
||||||
|
const selectSessionDeferred = createDeferred()
|
||||||
|
|
||||||
|
const hook = createRalphLoopHook({
|
||||||
|
directory: process.cwd(),
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
prompt: async (options: {
|
||||||
|
path: { id: string }
|
||||||
|
body: { parts: Array<{ type: string; text: string }> }
|
||||||
|
}) => {
|
||||||
|
promptCalls.push({
|
||||||
|
sessionID: options.path.id,
|
||||||
|
text: options.body.parts[0].text,
|
||||||
|
})
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
promptAsync: async (options: {
|
||||||
|
path: { id: string }
|
||||||
|
body: { parts: Array<{ type: string; text: string }> }
|
||||||
|
}) => {
|
||||||
|
promptCalls.push({
|
||||||
|
sessionID: options.path.id,
|
||||||
|
text: options.body.parts[0].text,
|
||||||
|
})
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
create: async (options: {
|
||||||
|
body: { parentID?: string; title?: string }
|
||||||
|
query?: { directory?: string }
|
||||||
|
}) => {
|
||||||
|
createSessionCalls.push({ parentID: options.body.parentID })
|
||||||
|
return { data: { id: `new-session-${createSessionCalls.length}` } }
|
||||||
|
},
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
},
|
||||||
|
tui: {
|
||||||
|
showToast: async () => ({}),
|
||||||
|
selectSession: async () => {
|
||||||
|
selectSessionCalls += 1
|
||||||
|
await selectSessionDeferred.promise
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Parameters<typeof createRalphLoopHook>[0])
|
||||||
|
|
||||||
|
hook.startLoop("session-old", "Build feature", { strategy: "reset" })
|
||||||
|
|
||||||
|
// when - first idle is in-flight and old session fires idle again before TUI switch resolves
|
||||||
|
const firstIdleEvent = hook.event({
|
||||||
|
event: { type: "session.idle", properties: { sessionID: "session-old" } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitUntil(() => selectSessionCalls > 0)
|
||||||
|
|
||||||
|
const secondIdleEvent = hook.event({
|
||||||
|
event: { type: "session.idle", properties: { sessionID: "session-old" } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitUntil(() => selectSessionCalls > 1)
|
||||||
|
|
||||||
|
selectSessionDeferred.resolve()
|
||||||
|
await Promise.all([firstIdleEvent, secondIdleEvent])
|
||||||
|
|
||||||
|
// then - second idle should not be skipped during reset transition
|
||||||
|
expect(createSessionCalls.length).toBe(2)
|
||||||
|
expect(promptCalls.length).toBe(2)
|
||||||
|
expect(hook.getState()?.iteration).toBe(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user