diff --git a/server/cmd/daemon/daemon.go b/server/cmd/daemon/daemon.go index e4008efb..ddc6e0a5 100644 --- a/server/cmd/daemon/daemon.go +++ b/server/cmd/daemon/daemon.go @@ -263,7 +263,7 @@ func (d *daemon) registerRuntimes(ctx context.Context) ([]daemonRuntime, error) continue } runtimes = append(runtimes, map[string]string{ - "name": fmt.Sprintf("Local %s", strings.Title(name)), + "name": fmt.Sprintf("Local %s", strings.ToUpper(name[:1])+name[1:]), "type": name, "version": version, "status": "online", @@ -293,13 +293,15 @@ func (d *daemon) registerRuntimes(ctx context.Context) ([]daemonRuntime, error) } func (d *daemon) ensurePaired(ctx context.Context) (string, error) { - // Use the first available agent for the pairing session metadata. + // Use a deterministic agent for the pairing session metadata (prefer codex for backward compat). var firstName string var firstEntry agentEntry - for name, entry := range d.cfg.Agents { - firstName = name - firstEntry = entry - break + for _, preferred := range []string{"codex", "claude"} { + if entry, ok := d.cfg.Agents[preferred]; ok { + firstName = preferred + firstEntry = entry + break + } } version, err := agent.DetectVersion(ctx, firstEntry.Path) if err != nil { diff --git a/server/cmd/daemon/daemon_test.go b/server/cmd/daemon/daemon_test.go index 99abc056..42efebc4 100644 --- a/server/cmd/daemon/daemon_test.go +++ b/server/cmd/daemon/daemon_test.go @@ -37,7 +37,7 @@ func TestResolveTaskWorkdirUsesRepoPathWhenPresent(t *testing.T) { } } -func TestBuildCodexPromptIncludesIssueAndSkills(t *testing.T) { +func TestBuildPromptIncludesIssueAndSkills(t *testing.T) { t.Parallel() prompt := buildPrompt(daemonTask{ diff --git a/server/pkg/agent/claude.go b/server/pkg/agent/claude.go index 7749ffbc..cb735936 100644 --- a/server/pkg/agent/claude.go +++ b/server/pkg/agent/claude.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log" "os" "os/exec" "strings" @@ -64,7 +65,7 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt cancel() return nil, fmt.Errorf("claude stdin pipe: %w", err) } - cmd.Stderr = os.Stderr + cmd.Stderr = newLogWriter(b.cfg.Logger, "[claude:stderr] ") if err := cmd.Start(); err != nil { cancel() @@ -73,7 +74,7 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt b.cfg.Logger.Printf("[claude] started pid=%d cwd=%s model=%s", cmd.Process.Pid, opts.Cwd, opts.Model) - msgCh := make(chan Message, 64) + msgCh := make(chan Message, 256) resCh := make(chan Result, 1) go func() { @@ -303,6 +304,8 @@ func trySend(ch chan<- Message, msg Message) { select { case ch <- msg: default: + // Channel full — drop message. Final output is accumulated separately + // in Result.Output, so only streaming consumers are affected. } } @@ -322,3 +325,21 @@ func detectCLIVersion(ctx context.Context, execPath string) (string, error) { } return strings.TrimSpace(string(data)), nil } + +// logWriter adapts a *log.Logger to an io.Writer for capturing stderr. +type logWriter struct { + logger *log.Logger + prefix string +} + +func newLogWriter(logger *log.Logger, prefix string) *logWriter { + return &logWriter{logger: logger, prefix: prefix} +} + +func (w *logWriter) Write(p []byte) (int, error) { + text := strings.TrimSpace(string(p)) + if text != "" { + w.logger.Printf("%s%s", w.prefix, text) + } + return len(p), nil +} diff --git a/server/pkg/agent/codex.go b/server/pkg/agent/codex.go index 95f68fbc..6028cc69 100644 --- a/server/pkg/agent/codex.go +++ b/server/pkg/agent/codex.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "os" "os/exec" "strings" "sync" @@ -49,7 +48,7 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti cancel() return nil, fmt.Errorf("codex stdin pipe: %w", err) } - cmd.Stderr = os.Stderr + cmd.Stderr = newLogWriter(b.cfg.Logger, "[codex:stderr] ") if err := cmd.Start(); err != nil { cancel() @@ -58,15 +57,24 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti b.cfg.Logger.Printf("[codex] started app-server pid=%d cwd=%s", cmd.Process.Pid, opts.Cwd) + msgCh := make(chan Message, 256) + resCh := make(chan Result, 1) + + var output strings.Builder + c := &codexClient{ cfg: b.cfg, stdin: stdin, pending: make(map[int]*pendingRPC), + // Set onMessage before starting the reader goroutine to avoid a race. + onMessage: func(msg Message) { + if msg.Type == MessageText { + output.WriteString(msg.Content) + } + trySend(msgCh, msg) + }, } - msgCh := make(chan Message, 64) - resCh := make(chan Result, 1) - // Start reading stdout in background go func() { scanner := bufio.NewScanner(stdout) @@ -76,7 +84,7 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti if line == "" { continue } - c.handleLine(line, msgCh) + c.handleLine(line) } c.closeAllPending(fmt.Errorf("codex process exited")) }() @@ -94,15 +102,6 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti startTime := time.Now() finalStatus := "completed" var finalError string - var output strings.Builder - - // Drain messages to accumulate output - c.onMessage = func(msg Message) { - if msg.Type == MessageText { - output.WriteString(msg.Content) - } - trySend(msgCh, msg) - } // 1. Initialize handshake _, err := c.request(runCtx, "initialize", map[string]any{ @@ -308,7 +307,7 @@ func (c *codexClient) closeAllPending(err error) { } } -func (c *codexClient) handleLine(line string, msgCh chan<- Message) { +func (c *codexClient) handleLine(line string) { var raw map[string]json.RawMessage if err := json.Unmarshal([]byte(line), &raw); err != nil { return