package cli import ( "context" "encoding/json" "fmt" "path/filepath" "regexp" "strings" "iop/apps/node/internal/runtime" "iop/packages/config" ) var ( codexSessionIDPattern = regexp.MustCompile(`(?mi)^session id:\s*(\S+)\s*$`) codexThreadStartedRegex = regexp.MustCompile(`\{[^{}]*"type"\s*:\s*"thread\.started"[^{}]*\}`) ) func isCodexExecProfile(profile config.CLIProfileConf) bool { return filepath.Base(profile.Command) == "codex" && len(profile.Args) > 0 && profile.Args[0] == "exec" } func (c *CLI) executeCodexExec(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error { sess, err := c.resolveCodexExecSession(spec) if err != nil { return err } sess.mu.Lock() defer sess.mu.Unlock() prompt := extractPrompt(spec.Input) args := codexExecArgs(profile.Args, sess.externalID, prompt) output, err := c.executeCommand(ctx, spec, profile, args, prompt, sink) if err != nil { return err } if externalID := parseCodexSessionID(output); externalID != "" { sess.externalID = externalID } if sess.externalID == "" { return fmt.Errorf("cli adapter: codex exec did not report a session id") } return nil } func (c *CLI) resolveCodexExecSession(spec runtime.ExecutionSpec) (*codexExecSession, error) { agent := cliAgentName(spec) key := sessionKey{agent: agent, sessionID: normalizeSessionID(spec.SessionID)} c.mu.Lock() defer c.mu.Unlock() if sess, ok := c.codexSessions[key]; ok { return sess, nil } if spec.SessionMode == runtime.SessionModeRequireExisting { return nil, fmt.Errorf("cli adapter: no persistent session for agent %q session %q", agent, key.sessionID) } sess := &codexExecSession{key: key} c.codexSessions[key] = sess return sess, nil } func codexExecArgs(configured []string, externalSessionID, prompt string) []string { if externalSessionID == "" { args := append([]string{}, configured...) return append(args, prompt) } args := []string{"exec", "resume"} args = append(args, codexResumeOptions(configured)...) args = append(args, externalSessionID, prompt) return args } func codexResumeOptions(configured []string) []string { if len(configured) > 0 && configured[0] == "exec" { configured = configured[1:] } out := make([]string, 0, len(configured)) for i := 0; i < len(configured); i++ { arg := configured[i] switch { case arg == "--color" || arg == "-C" || arg == "--cd" || arg == "-s" || arg == "--sandbox" || arg == "--profile" || arg == "-p" || arg == "--local-provider" || arg == "--add-dir" || arg == "--output-schema": if i+1 < len(configured) { i++ } continue case strings.HasPrefix(arg, "--color=") || strings.HasPrefix(arg, "--cd=") || strings.HasPrefix(arg, "--sandbox=") || strings.HasPrefix(arg, "--profile=") || strings.HasPrefix(arg, "--local-provider=") || strings.HasPrefix(arg, "--add-dir=") || strings.HasPrefix(arg, "--output-schema="): continue default: out = append(out, arg) } } return out } func parseCodexSessionID(output string) string { if matches := codexSessionIDPattern.FindStringSubmatch(output); len(matches) >= 2 { return matches[1] } for _, raw := range codexThreadStartedRegex.FindAllString(output, -1) { var ev struct { Type string `json:"type"` ThreadID string `json:"thread_id"` } if err := json.Unmarshal([]byte(raw), &ev); err == nil && ev.Type == "thread.started" && ev.ThreadID != "" { return ev.ThreadID } } return "" }