Skip to content

Commit 5a631e1

Browse files
committed
fix(docs): bound i18n codex prompt cleanup
1 parent a548d8e commit 5a631e1

4 files changed

Lines changed: 93 additions & 5 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build !windows
2+
3+
package main
4+
5+
import (
6+
"errors"
7+
"os"
8+
"os/exec"
9+
"syscall"
10+
)
11+
12+
func configureCodexPromptCommand(command *exec.Cmd) {
13+
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
14+
command.Cancel = func() error {
15+
if command.Process == nil {
16+
return os.ErrProcessDone
17+
}
18+
err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL)
19+
if errors.Is(err, syscall.ESRCH) {
20+
return os.ErrProcessDone
21+
}
22+
return err
23+
}
24+
command.WaitDelay = docsI18nCommandWaitDelay()
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build windows
2+
3+
package main
4+
5+
import "os/exec"
6+
7+
func configureCodexPromptCommand(command *exec.Cmd) {
8+
command.WaitDelay = docsI18nCommandWaitDelay()
9+
}

scripts/docs-i18n/translator.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import (
1414
)
1515

1616
const (
17-
translateMaxAttempts = 3
18-
translateBaseDelay = 15 * time.Second
19-
defaultPromptTimeout = 2 * time.Minute
20-
envDocsI18nPromptTimeout = "OPENCLAW_DOCS_I18N_PROMPT_TIMEOUT"
21-
envDocsI18nCodexExecutable = "OPENCLAW_DOCS_I18N_CODEX_EXECUTABLE"
17+
translateMaxAttempts = 3
18+
translateBaseDelay = 15 * time.Second
19+
defaultPromptTimeout = 2 * time.Minute
20+
defaultCommandWaitDelay = 15 * time.Second
21+
envDocsI18nPromptTimeout = "OPENCLAW_DOCS_I18N_PROMPT_TIMEOUT"
22+
envDocsI18nCommandWaitDelay = "OPENCLAW_DOCS_I18N_COMMAND_WAIT_DELAY"
23+
envDocsI18nCodexExecutable = "OPENCLAW_DOCS_I18N_CODEX_EXECUTABLE"
2224
)
2325

2426
var errEmptyTranslation = errors.New("empty translation")
@@ -214,6 +216,7 @@ func runCodexExecPrompt(ctx context.Context, req codexPromptRequest) (string, er
214216
"-",
215217
}
216218
command := exec.CommandContext(ctx, docsCodexExecutable(), args...)
219+
configureCodexPromptCommand(command)
217220
command.Stdin = strings.NewReader(buildCodexTranslationPrompt(req.SystemPrompt, req.Message))
218221
command.Env = append(os.Environ(), "CODEX_HOME="+codexHome)
219222
var stdout bytes.Buffer
@@ -327,3 +330,15 @@ func docsI18nPromptTimeout() time.Duration {
327330
}
328331
return parsed
329332
}
333+
334+
func docsI18nCommandWaitDelay() time.Duration {
335+
value := strings.TrimSpace(os.Getenv(envDocsI18nCommandWaitDelay))
336+
if value == "" {
337+
return defaultCommandWaitDelay
338+
}
339+
parsed, err := time.ParseDuration(value)
340+
if err != nil || parsed <= 0 {
341+
return defaultCommandWaitDelay
342+
}
343+
return parsed
344+
}

scripts/docs-i18n/translator_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ func TestDocsI18nPromptTimeoutUsesEnvOverride(t *testing.T) {
5656
}
5757
}
5858

59+
func TestDocsI18nCommandWaitDelayUsesEnvOverride(t *testing.T) {
60+
t.Setenv(envDocsI18nCommandWaitDelay, "50ms")
61+
62+
if got := docsI18nCommandWaitDelay(); got != 50*time.Millisecond {
63+
t.Fatalf("expected 50ms wait delay, got %s", got)
64+
}
65+
}
66+
5967
func TestIsRetryableTranslateErrorRejectsDeadlineExceeded(t *testing.T) {
6068
t.Parallel()
6169

@@ -235,6 +243,37 @@ printf 'translated from codex\n' > "$out"
235243
}
236244
}
237245

246+
func TestRunCodexExecPromptDoesNotHangOnInheritedPipesAfterTimeout(t *testing.T) {
247+
dir := t.TempDir()
248+
fakeCodex := filepath.Join(dir, "codex")
249+
if err := os.WriteFile(fakeCodex, []byte(`#!/bin/sh
250+
set -eu
251+
(sleep 10) &
252+
sleep 10
253+
`), 0o755); err != nil {
254+
t.Fatalf("write fake codex: %v", err)
255+
}
256+
t.Setenv(envDocsI18nCodexExecutable, fakeCodex)
257+
t.Setenv(envDocsI18nCommandWaitDelay, "20ms")
258+
t.Setenv("OPENAI_API_KEY", "test-openai-key")
259+
260+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
261+
defer cancel()
262+
started := time.Now()
263+
_, err := runCodexExecPrompt(ctx, codexPromptRequest{
264+
SystemPrompt: "Translate.",
265+
Message: "Hello",
266+
Model: "gpt-5.5",
267+
Thinking: "high",
268+
})
269+
if err == nil {
270+
t.Fatal("expected timeout error")
271+
}
272+
if elapsed := time.Since(started); elapsed > 2*time.Second {
273+
t.Fatalf("expected bounded timeout, took %s", elapsed)
274+
}
275+
}
276+
238277
func TestPreviewCommandOutputFlattensAndTruncates(t *testing.T) {
239278
input := "line one\n\nline two\tline three " + strings.Repeat("x", 600)
240279
preview := previewCommandOutput(input, "")

0 commit comments

Comments
 (0)