refactor: remove cli run timeout path and rely on strict completion
This commit is contained in:
parent
7b2c2529fe
commit
4d3cce685d
@ -67,10 +67,9 @@ program
|
||||
.command("run <message>")
|
||||
.allowUnknownOption()
|
||||
.passThroughOptions()
|
||||
.description("Run opencode with todo/background task completion enforcement")
|
||||
.description("Run opencode with todo/background task completion enforcement")
|
||||
.option("-a, --agent <name>", "Agent to use (default: from CLI/env/config, fallback: Sisyphus)")
|
||||
.option("-d, --directory <path>", "Working directory")
|
||||
.option("-t, --timeout <ms>", "Timeout in milliseconds (default: 30 minutes)", parseInt)
|
||||
.option("-p, --port <port>", "Server port (attaches if port already in use)", parseInt)
|
||||
.option("--attach <url>", "Attach to existing opencode server URL")
|
||||
.option("--on-complete <command>", "Shell command to run after completion")
|
||||
@ -81,7 +80,6 @@ program
|
||||
Examples:
|
||||
$ bunx oh-my-opencode run "Fix the bug in index.ts"
|
||||
$ bunx oh-my-opencode run --agent Sisyphus "Implement feature X"
|
||||
$ bunx oh-my-opencode run --timeout 3600000 "Large refactoring task"
|
||||
$ bunx oh-my-opencode run --port 4321 "Fix the bug"
|
||||
$ bunx oh-my-opencode run --attach http://127.0.0.1:4321 "Fix the bug"
|
||||
$ bunx oh-my-opencode run --json "Fix the bug" | jq .sessionId
|
||||
@ -110,7 +108,6 @@ Unlike 'opencode run', this command waits until:
|
||||
message,
|
||||
agent: options.agent,
|
||||
directory: options.directory,
|
||||
timeout: options.timeout,
|
||||
port: options.port,
|
||||
attach: options.attach,
|
||||
onComplete: options.onComplete,
|
||||
|
||||
@ -336,68 +336,4 @@ describe("pollForCompletion", () => {
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
it("returns 130 after graceful timeout window expires", async () => {
|
||||
//#given
|
||||
spyOn(console, "log").mockImplementation(() => {})
|
||||
spyOn(console, "error").mockImplementation(() => {})
|
||||
const ctx = createMockContext({
|
||||
statuses: {
|
||||
"test-session": { type: "busy" },
|
||||
},
|
||||
})
|
||||
const eventState = createEventState()
|
||||
eventState.mainSessionIdle = false
|
||||
eventState.hasReceivedMeaningfulWork = true
|
||||
const abortController = new AbortController()
|
||||
|
||||
//#when
|
||||
const result = await pollForCompletion(ctx, eventState, abortController, {
|
||||
pollIntervalMs: 10,
|
||||
requiredConsecutive: 1,
|
||||
minStabilizationMs: 0,
|
||||
timeoutMs: 30,
|
||||
timeoutGraceMs: 40,
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(result).toBe(130)
|
||||
expect(abortController.signal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it("allows completion during graceful timeout window", async () => {
|
||||
//#given
|
||||
spyOn(console, "log").mockImplementation(() => {})
|
||||
spyOn(console, "error").mockImplementation(() => {})
|
||||
const ctx = createMockContext()
|
||||
const eventState = createEventState()
|
||||
eventState.mainSessionIdle = true
|
||||
eventState.hasReceivedMeaningfulWork = true
|
||||
const abortController = new AbortController()
|
||||
let todoCalls = 0
|
||||
|
||||
;(ctx.client.session as unknown as {
|
||||
todo: ReturnType<typeof mock>
|
||||
children: ReturnType<typeof mock>
|
||||
status: ReturnType<typeof mock>
|
||||
}).todo = mock(async () => {
|
||||
todoCalls++
|
||||
if (todoCalls === 1) {
|
||||
return { data: [{ id: "1", content: "wip", status: "in_progress", priority: "high" }] }
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
|
||||
//#when
|
||||
const result = await pollForCompletion(ctx, eventState, abortController, {
|
||||
pollIntervalMs: 10,
|
||||
requiredConsecutive: 1,
|
||||
minStabilizationMs: 0,
|
||||
timeoutMs: 20,
|
||||
timeoutGraceMs: 80,
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(result).toBe(0)
|
||||
expect(abortController.signal.aborted).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,14 +8,11 @@ const DEFAULT_POLL_INTERVAL_MS = 500
|
||||
const DEFAULT_REQUIRED_CONSECUTIVE = 3
|
||||
const ERROR_GRACE_CYCLES = 3
|
||||
const MIN_STABILIZATION_MS = 10_000
|
||||
const DEFAULT_TIMEOUT_GRACE_MS = 15_000
|
||||
|
||||
export interface PollOptions {
|
||||
pollIntervalMs?: number
|
||||
requiredConsecutive?: number
|
||||
minStabilizationMs?: number
|
||||
timeoutMs?: number
|
||||
timeoutGraceMs?: number
|
||||
}
|
||||
|
||||
export async function pollForCompletion(
|
||||
@ -29,13 +26,10 @@ export async function pollForCompletion(
|
||||
options.requiredConsecutive ?? DEFAULT_REQUIRED_CONSECUTIVE
|
||||
const minStabilizationMs =
|
||||
options.minStabilizationMs ?? MIN_STABILIZATION_MS
|
||||
const timeoutMs = options.timeoutMs ?? 0
|
||||
const timeoutGraceMs = options.timeoutGraceMs ?? DEFAULT_TIMEOUT_GRACE_MS
|
||||
let consecutiveCompleteChecks = 0
|
||||
let errorCycleCount = 0
|
||||
let firstWorkTimestamp: number | null = null
|
||||
const pollStartTimestamp = Date.now()
|
||||
let timeoutNoticePrinted = false
|
||||
|
||||
while (!abortController.signal.aborted) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
||||
@ -44,20 +38,6 @@ export async function pollForCompletion(
|
||||
return 130
|
||||
}
|
||||
|
||||
if (timeoutMs > 0) {
|
||||
const elapsedMs = Date.now() - pollStartTimestamp
|
||||
if (elapsedMs >= timeoutMs && !timeoutNoticePrinted) {
|
||||
console.log(pc.yellow("\nTimeout reached. Entering graceful shutdown window..."))
|
||||
timeoutNoticePrinted = true
|
||||
}
|
||||
|
||||
if (elapsedMs >= timeoutMs + timeoutGraceMs) {
|
||||
console.log(pc.yellow("Grace period expired. Aborting..."))
|
||||
abortController.abort()
|
||||
return 130
|
||||
}
|
||||
}
|
||||
|
||||
// ERROR CHECK FIRST — errors must not be masked by other gates
|
||||
if (eventState.mainSessionError) {
|
||||
errorCycleCount++
|
||||
|
||||
@ -11,7 +11,6 @@ import { pollForCompletion } from "./poll-for-completion"
|
||||
|
||||
export { resolveRunAgent }
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 600_000
|
||||
const EVENT_PROCESSOR_SHUTDOWN_TIMEOUT_MS = 2_000
|
||||
|
||||
export async function waitForEventProcessorShutdown(
|
||||
@ -39,7 +38,6 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
const {
|
||||
message,
|
||||
directory = process.cwd(),
|
||||
timeout = DEFAULT_TIMEOUT_MS,
|
||||
} = options
|
||||
|
||||
const jsonManager = options.json ? createJsonOutputManager() : null
|
||||
@ -99,9 +97,7 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
})
|
||||
|
||||
console.log(pc.dim("Waiting for completion...\n"))
|
||||
const exitCode = await pollForCompletion(ctx, eventState, abortController, {
|
||||
timeoutMs: timeout,
|
||||
})
|
||||
const exitCode = await pollForCompletion(ctx, eventState, abortController)
|
||||
|
||||
// Abort the event stream to stop the processor
|
||||
abortController.abort()
|
||||
|
||||
@ -6,7 +6,6 @@ export interface RunOptions {
|
||||
agent?: string
|
||||
verbose?: boolean
|
||||
directory?: string
|
||||
timeout?: number
|
||||
port?: number
|
||||
attach?: string
|
||||
onComplete?: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user