fix(opencode): advertise configured shell timeout (#28998)

Co-authored-by: Nabs <nabil@instafork.com>
This commit is contained in:
Aiden Cline 2026-05-23 10:43:39 -05:00 committed by GitHub
parent 387c5a02c8
commit ba437069e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 36 additions and 19 deletions

View file

@ -340,7 +340,7 @@ export const ShellTool = Tool.define(
const trunc = yield* Truncate.Service
const plugin = yield* Plugin.Service
const flags = yield* RuntimeFlags.Service
const defaultTimeout = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
const defaultTimeoutMs = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) {
const lines = yield* spawner
@ -601,7 +601,7 @@ export const ShellTool = Tool.define(
const shell = Shell.acceptable(cfg.shell)
const name = Shell.name(shell)
const limits = yield* trunc.limits()
const prompt = ShellPrompt.render(name, process.platform, limits)
const prompt = ShellPrompt.render(name, process.platform, limits, defaultTimeoutMs)
log.info("shell tool using shell", { shell })
return {
@ -616,7 +616,7 @@ export const ShellTool = Tool.define(
if (params.timeout !== undefined && params.timeout < 0) {
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
}
const timeout = params.timeout ?? defaultTimeout
const timeout = params.timeout ?? defaultTimeoutMs
const ps = Shell.ps(shell)
yield* Effect.scoped(
Effect.gen(function* () {

View file

@ -83,7 +83,7 @@ function chainGuidance(name: string) {
return "If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead."
}
function bashCommandSection(chain: string, limits: Limits) {
function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
return `Before executing the command, please follow these steps:
1. Directory Verification:
@ -102,7 +102,7 @@ function bashCommandSection(chain: string, limits: Limits) {
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`head\`, \`tail\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
@ -127,7 +127,13 @@ Usage notes:
</bad-example>`
}
function powershellCommandSection(name: string, chain: string, pathSep: string, limits: Limits) {
function powershellCommandSection(
name: string,
chain: string,
pathSep: string,
limits: Limits,
defaultTimeoutMs: number,
) {
return `${powershellNotes(name)}
Before executing the command, please follow these steps:
@ -148,7 +154,7 @@ Before executing the command, please follow these steps:
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`Select-Object -First\`, \`Select-Object -Last\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
@ -173,7 +179,7 @@ Usage notes:
</bad-example>`
}
function cmdCommandSection(chain: string, limits: Limits) {
function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
return `# cmd.exe shell notes
- Use double quotes for paths with spaces.
- Use %VAR% for environment variables.
@ -198,7 +204,7 @@ Before executing the command, please follow these steps:
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`more\` or other pagination commands to limit output; the full output will already be captured to a file for more precise searching.
@ -223,7 +229,7 @@ Usage notes:
</bad-example>`
}
function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
const isPowerShell = PS.has(name)
const chain = chainGuidance(name)
if (CMD.has(name)) {
@ -231,7 +237,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
workdirSection:
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
commandSection: cmdCommandSection(chain, limits),
commandSection: cmdCommandSection(chain, limits, defaultTimeoutMs),
gitCommands: "git commands",
gitCommandRestriction: "git commands",
createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
@ -244,7 +250,13 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
workdirSection:
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
commandSection: powershellCommandSection(name, chain, platform === "win32" ? "\\" : "/", limits),
commandSection: powershellCommandSection(
name,
chain,
platform === "win32" ? "\\" : "/",
limits,
defaultTimeoutMs,
),
gitCommands: "git commands",
gitCommandRestriction: "git commands",
createPrInstruction: "Create PR using gh pr create with a PowerShell here-string to pass the body correctly.",
@ -260,7 +272,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
workdirSection:
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.",
commandSection: bashCommandSection(chain, limits),
commandSection: bashCommandSection(chain, limits, defaultTimeoutMs),
gitCommands: "bash commands",
gitCommandRestriction: "git bash commands",
createPrInstruction:
@ -272,8 +284,8 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
}
}
export function render(name: string, platform: NodeJS.Platform, limits: Limits) {
const selected = profile(name, platform, limits)
export function render(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
const selected = profile(name, platform, limits, defaultTimeoutMs)
return {
description: renderPrompt(DESCRIPTION, {
intro: selected.intro,

View file

@ -1085,10 +1085,15 @@ describe("tool.shell abort", () => {
runIn(
projectRoot,
Effect.gen(function* () {
const result = yield* run({
command: `echo started && sleep 60`,
description: "Default timeout test",
})
const tool = yield* initShell()
expect(tool.description).toContain("commands will time out after 500ms")
const result = yield* tool.execute(
{
command: `echo started && sleep 60`,
description: "Default timeout test",
},
ctx,
)
expect(result.output).toContain("started")
expect(result.output).toContain("exceeding timeout 500 ms")
}),