package cli import ( "context" "encoding/json" "fmt" "regexp" "iop/apps/node/internal/runtime" "iop/packages/go/config" ) var ( codexSessionIDPattern = regexp.MustCompile(`(?mi)^session id:\s*(\S+)\s*$`) codexThreadStartedRegex = regexp.MustCompile(`\{[^{}]*"type"\s*:\s*"thread\.started"[^{}]*\}`) ) func (e *codexExecutor) Execute(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error { if profile.ResumeArgs == nil { return fmt.Errorf("cli adapter: codex-exec mode requires resume_args in profile %q", spec.Target) } sess, err := e.resolveCodexExecSession(spec) if err != nil { return err } sess.mu.Lock() defer sess.mu.Unlock() prompt := extractPrompt(spec.Input) args := codexExecArgs(profile, sess.externalID, prompt) output, err := e.cli.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 (e *codexExecutor) resolveCodexExecSession(spec runtime.ExecutionSpec) (*codexExecSession, error) { target := cliTargetName(spec) key := sessionKey{target: target, sessionID: normalizeSessionID(spec.SessionID)} e.mu.Lock() defer e.mu.Unlock() if sess, ok := e.sessions[key]; ok { return sess, nil } if spec.SessionMode == runtime.SessionModeRequireExisting { return nil, fmt.Errorf("cli adapter: no persistent session for target %q session %q", target, key.sessionID) } sess := &codexExecSession{key: key} e.sessions[key] = sess return sess, nil } func (e *codexExecutor) Sessions() []sessionListEntry { e.mu.Lock() defer e.mu.Unlock() snaps := make([]sessionListEntry, 0, len(e.sessions)) for k := range e.sessions { snaps = append(snaps, sessionListEntry{"codex-exec", k.target, k.sessionID}) } return snaps } func (e *codexExecutor) Terminate(ctx context.Context, target, sessionID string) (bool, error) { key := sessionKey{target: target, sessionID: normalizeSessionID(sessionID)} e.mu.Lock() _, ok := e.sessions[key] if ok { delete(e.sessions, key) } e.mu.Unlock() return ok, nil } func (e *codexExecutor) Stop(ctx context.Context) error { e.mu.Lock() e.sessions = make(map[sessionKey]*codexExecSession) e.mu.Unlock() return nil } func codexExecArgs(profile config.CLIProfileConf, externalSessionID, prompt string) []string { if externalSessionID == "" { args := append([]string{}, profile.Args...) return append(args, prompt) } args := append([]string{}, profile.ResumeArgs...) return append(args, externalSessionID, prompt) } 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 "" }