package execenv import ( "fmt" "log/slog" "os" "regexp" "strings" ) // Background // // Recent Codex `app-server` releases enable `features.multi_agent` by // default, exposing spawn_agent / send_input / wait / resume_agent / // close_agent tools to the model so a Codex thread can fan out into nested // subagents. The Multica daemon currently models only the parent Codex // thread per task: when the parent emits `turn/completed`, the task is // marked terminal even if spawned children are still running or have not // flushed their output. The result is a class of premature-completion // failures where useful child-agent work is dropped. // // Until either Codex exposes a "parent done but children still open" // lifecycle state with drain/cancel primitives, or the Multica runtime // models child threads as first-class task entities, the daemon disables // Codex native multi-agent by default for daemon-managed task sessions. // The override only touches the per-task `CODEX_HOME/config.toml`; the // user's global `~/.codex/config.toml` is never modified. // // Users who explicitly want Codex native subagents inside a Multica task // (and accept the lifecycle risk) can keep the feature enabled by setting // `MULTICA_CODEX_MULTI_AGENT=1` in the daemon environment. // // Layout note // // TOML rejects redefining a table that has already been created — including // implicitly via a dotted key. So the managed block must adapt to the // user's existing config: // // - If the user's config contains a top-level `[features]` table, the // managed `multi_agent = false` is injected INSIDE that table (with // marker comments). Writing `features.multi_agent = false` at the // TOML root would implicitly redefine the same `features` table and // the strict TOML parser used by Codex (`toml-rs`) would fail with // `table 'features' already exists`. // - Otherwise, the managed block lives at the top of the file with the // dotted-key form `features.multi_agent = false`. // MulticaCodexMultiAgentEnv is the env var users can set to keep Codex // native multi-agent enabled inside daemon-managed tasks. Anything truthy // (1, true, yes, on; case-insensitive) keeps the feature on; everything // else (including unset) disables it. const MulticaCodexMultiAgentEnv = "MULTICA_CODEX_MULTI_AGENT" // multicaMultiAgentBeginMarker / multicaMultiAgentEndMarker delimit the // multi-agent-specific managed block. Kept separate from the sandbox // block so each can evolve and migrate independently. const ( multicaMultiAgentBeginMarker = "# BEGIN multica-managed multi-agent (do not edit; regenerated by daemon)" multicaMultiAgentEndMarker = "# END multica-managed multi-agent" ) // `\n*` rather than `\n?` so reruns don't accumulate blank lines when this // block coexists with the sandbox managed block in the same file. The same // regex matches the block whether it sits at the file root (dotted-key // form) or inside a `[features]` table — only the body keys differ. var multiAgentBlockRe = regexp.MustCompile( `(?ms)^` + regexp.QuoteMeta(multicaMultiAgentBeginMarker) + `.*?^` + regexp.QuoteMeta(multicaMultiAgentEndMarker) + `\n*`) var ( // matches a top-level `[features]` table header, allowing TOML's optional // whitespace inside brackets and inline comments after the header. rootFeaturesTableHeaderRe = regexp.MustCompile(`^\s*\[\s*features\s*\]\s*(?:#.*)?$`) // matches `multi_agent = ...` (with optional whitespace) inside a // `[features]` table. featuresTableMultiAgentRe = regexp.MustCompile(`^\s*multi_agent\s*=`) // matches `features.multi_agent = ...` at the TOML root (top-level // dotted-key form, including TOML's optional whitespace around dots). rootDottedMultiAgentRe = regexp.MustCompile(`^\s*features\s*\.\s*multi_agent\s*=`) ) // codexMultiAgentEnabled reports whether the user opted into keeping Codex // native multi-agent on for daemon-managed tasks. func codexMultiAgentEnabled() bool { raw := strings.TrimSpace(os.Getenv(MulticaCodexMultiAgentEnv)) switch strings.ToLower(raw) { case "1", "true", "yes", "on": return true } return false } // renderMulticaMultiAgentBlock returns the daemon-managed multi-agent // block. The body uses `multi_agent = false` when injected inside a // `[features]` table, and `features.multi_agent = false` otherwise. func renderMulticaMultiAgentBlock(inFeaturesTable bool) string { var b strings.Builder b.WriteString(multicaMultiAgentBeginMarker) b.WriteString("\n") if inFeaturesTable { b.WriteString("multi_agent = false\n") } else { b.WriteString("features.multi_agent = false\n") } b.WriteString(multicaMultiAgentEndMarker) b.WriteString("\n") return b.String() } // stripUserMultiAgentDirectives removes any `features.multi_agent = ...` // line at the TOML root (dotted-key form), plus any `multi_agent = ...` // line that sits inside a top-level `[features]` table. Both forms encode // the same TOML key and would conflict with the managed block. // // Other tables (`[features.experimental]`, `[profiles.foo]`, ...) are // preserved untouched: they live under their own scope and don't redefine // `features.multi_agent` at the root. func stripUserMultiAgentDirectives(content string) string { lines := strings.Split(content, "\n") out := make([]string, 0, len(lines)) currentTable := "" // empty = TOML root for _, line := range lines { trimmed := strings.TrimSpace(line) if rootFeaturesTableHeaderRe.MatchString(line) { currentTable = "[features]" out = append(out, line) continue } if strings.HasPrefix(trimmed, "[") { currentTable = trimmed out = append(out, line) continue } switch currentTable { case "": if rootDottedMultiAgentRe.MatchString(trimmed) { continue } case "[features]": if featuresTableMultiAgentRe.MatchString(trimmed) { continue } } out = append(out, line) } return strings.Join(out, "\n") } // hasRootFeaturesTable reports whether the file contains a top-level // `[features]` table header. Sub-tables like `[features.experimental]` do // NOT count: they implicitly create `features` but don't conflict with a // root-level `features.multi_agent` dotted key. func hasRootFeaturesTable(content string) bool { for _, line := range strings.Split(content, "\n") { if rootFeaturesTableHeaderRe.MatchString(line) { return true } } return false } // injectManagedBlockIntoFeaturesTable inserts the in-table managed block // immediately after the first `[features]` header line. Caller must have // already stripped any prior managed block and any user-set `multi_agent` // directive from inside the table. func injectManagedBlockIntoFeaturesTable(content string) string { block := renderMulticaMultiAgentBlock(true) // Drop the trailing `\n` so we don't introduce a stray blank line when // splicing block lines between existing lines. blockLines := strings.Split(strings.TrimRight(block, "\n"), "\n") lines := strings.Split(content, "\n") for i, line := range lines { if !rootFeaturesTableHeaderRe.MatchString(line) { continue } out := make([]string, 0, len(lines)+len(blockLines)) out = append(out, lines[:i+1]...) out = append(out, blockLines...) out = append(out, lines[i+1:]...) return strings.Join(out, "\n") } return content } // ensureCodexMultiAgentConfig writes the daemon-managed multi-agent block // into the per-task config.toml so Codex native subagents stay disabled. // Idempotent: running it twice produces the same file. // // When MULTICA_CODEX_MULTI_AGENT is set to a truthy value, the function is // a no-op — the user has explicitly opted into Codex native subagents and // accepts the lifecycle risk. Toggling the env var across prepare runs is // not supported: the per-task config is short-lived (recreated per task), // so users should set the var once at daemon start. func ensureCodexMultiAgentConfig(configPath string, logger *slog.Logger) error { if codexMultiAgentEnabled() { if logger != nil { logger.Info("codex multi-agent: leaving Codex native multi-agent untouched per MULTICA_CODEX_MULTI_AGENT", "config_path", configPath, ) } return nil } data, err := os.ReadFile(configPath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("read config.toml: %w", err) } existing := string(data) // Always strip any previously written managed block (root or in-table // form) so reruns and layout transitions stay clean. existing = multiAgentBlockRe.ReplaceAllString(existing, "") // Strip user-set directives in both encodings; the managed block re-adds // the canonical form below. existing = stripUserMultiAgentDirectives(existing) var updated string if hasRootFeaturesTable(existing) { updated = injectManagedBlockIntoFeaturesTable(existing) } else { existing = strings.TrimLeft(existing, "\n") block := renderMulticaMultiAgentBlock(false) if existing == "" { updated = block } else { updated = block + "\n" + existing } } if updated == string(data) { return nil } if err := os.WriteFile(configPath, []byte(updated), 0o644); err != nil { return fmt.Errorf("write config.toml: %w", err) } return nil }