refactor: remove cli run timeout path and rely on strict completion

This commit is contained in:
YeonGyu-Kim 2026-02-17 14:52:52 +09:00
parent 7b2c2529fe
commit 4d3cce685d
5 changed files with 2 additions and 94 deletions

View File

@ -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,

View File

@ -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)
})
})

View File

@ -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++

View File

@ -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()

View File

@ -6,7 +6,6 @@ export interface RunOptions {
agent?: string
verbose?: boolean
directory?: string
timeout?: number
port?: number
attach?: string
onComplete?: string