Skip to content

feat(codex): add opt-in native subagent defaults - #498

Draft
wonsh42 wants to merge 1 commit into
lidge-jun:devfrom
wonsh42:agent/sync-native-subagent-defaults
Draft

feat(codex): add opt-in native subagent defaults#498
wonsh42 wants to merge 1 commit into
lidge-jun:devfrom
wonsh42:agent/sync-native-subagent-defaults

Conversation

@wonsh42

@wonsh42 wonsh42 commented Jul 26, 2026

Copy link
Copy Markdown

Summary

  • Add a default-off syncCodexSubagentDefaults option that applies the selected injectionModel and optional injectionEffort to Codex's native [agents] defaults on sync or restart.
  • Keep native defaults independent from OpenCodex multi-agent guidance: both consume the shared model/effort selection, but neither setting triggers delegation or acts as a proxy-side spawn router.
  • Preserve user ownership with marker-scoped, format-preserving TOML edits. Unmarked defaults remain authoritative, conflicting pairs are left untouched, and ambiguous ownership refuses injection without changing the file.
  • Keep restore safe: journal snapshots exclude managed-default residue, unchanged state restores exactly, and edited configs remove only OpenCodex-owned values while preserving later user changes.
  • Extend GET/PUT /api/injection-model, the Dashboard, localized UI text, user documentation, and maintainer structure docs for the opt-in.
  • Propagate native-default conflicts and routing-ownership blocks through a structured nativeSubagentDefaultsWarning; failed sync and restore paths now report actionable errors instead of claiming success.

Why

The Dashboard's subagent model and effort could inform OpenCodex-authored delegation guidance, but they did not populate Codex's native subagent defaults. This adds an explicit and reversible path for newly created Codex tasks while keeping existing user-authored configuration authoritative.

Safety and ownership

  • The feature remains disabled unless the user opts in and has selected a model.
  • OpenCodex writes only immediately marker-owned agents.default_subagent_model and agents.default_subagent_reasoning_effort values.
  • Comments, ordering, unknown keys, quoted keys, multiline TOML data, and the existing line-ending style are preserved.
  • A user-owned target value blocks the complete model/effort pair rather than creating a partially managed result.
  • External model_provider configurations and a user-owned root openai_base_url block native-default injection and return a structured warning.
  • Invalid persisted opt-in/model/effort values disable only native-default sync and emit diagnostics; unrelated providers, accounts, and listen settings remain available.
  • Existing tasks are unchanged; synchronized defaults apply only when Codex creates a new task.
  • No personal configuration files are included. In particular, ~/.codex/config.toml and ~/.opencodex/config.json are runtime inputs or mutation targets, not committed artifacts.

Scope

This PR intentionally excludes broad service-manager changes, provider work, v2 surface/routing changes, and general concurrency hardening. Lifecycle edits are limited to accurately reporting sync and native-restore failures.

Verification

  • bun run test — 4,667 pass, 0 fail across 356 files.
  • bun run typecheck — passed.
  • bun run lint:gui — passed.
  • Impacted GUI tests — 13 pass, 0 fail.
  • Full GUI suite — 278 pass and one order-dependent failure in debug-put-install-order.test.tsx; the same assertion fails on upstream/dev (270 pass, 1 fail), while every impacted test passes in isolation.
  • cd gui && bun run build — passed (164 modules).
  • cd docs-site && bun run build — passed (131 pages).
  • bun run privacy:scan — passed.
  • git diff --check upstream/dev — passed.
  • Installed Codex 0.145.0 accepted both native subagent-default keys.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features

    • Added an optional dashboard setting to sync selected model and reasoning effort as native Codex sub-agent defaults for new tasks.
    • Clarified that delegation guidance and native defaults are independent controls, not per-spawn routing.
    • Improved dashboard messaging for default synchronization warnings and model-clearing behavior.
    • Stop, sync, and restore operations now clearly report failures and return appropriate error status.
  • Documentation

    • Updated configuration, dashboard, and sub-agent guidance across supported languages to explain the revised behavior and API semantics.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds opt-in synchronization of selected subagent model and reasoning defaults into Codex configuration, with ownership-aware TOML editing, API and dashboard controls, localized documentation, and failure propagation through restore, sync, shutdown, and stop flows.

Changes

Native subagent defaults

Layer / File(s) Summary
Managed defaults transformation and injection
src/codex/subagent-defaults.ts, src/codex/inject.ts, src/codex/sync.ts
Adds marker-owned TOML editing, conflict detection, ambiguity refusal, cleanup, restoration, and warning propagation for native subagent defaults.
Configuration and management API contract
src/types.ts, src/config.ts, src/server/management/*, structure/*
Adds syncCodexSubagentDefaults, validates dependent model and effort values, normalizes malformed persisted settings, and exposes partial GET/PUT API semantics.
Dashboard synchronization controls
gui/src/pages/*, gui/src/i18n/*, gui/src/App.tsx, gui/src/stop-proxy.ts
Adds shared injection saving, native-default state, warning rendering, localized labels, and typed proxy-stop outcomes.
Lifecycle and validation
src/cli/index.ts, tests/*, gui/tests/*
Propagates restore and sync failures to exit codes and covers TOML transforms, API validation, dashboard behavior, stop handling, and integration flows.
Localized documentation
docs-site/src/content/docs/**/*
Documents guidance injection, native Codex defaults, API behavior, model inheritance, featured-model behavior, and non-router semantics across locales.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant InjectionAPI
  participant CodexSync
  participant ConfigToml
  Dashboard->>InjectionAPI: PUT model, effort, and syncCodexSubagentDefaults
  InjectionAPI->>CodexSync: persist validated settings
  CodexSync->>ConfigToml: apply marker-owned native defaults
  ConfigToml-->>CodexSync: updated content or conflict warning
  CodexSync-->>Dashboard: sync result and native-default warning
Loading

Possibly related PRs

Suggested reviewers: lidge-jun, wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an opt-in native subagent defaults sync feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 26, 2026
@wonsh42

wonsh42 commented Jul 26, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/index.ts (1)

213-243: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard restoreNativeCodex() before continuing cleanup src/cli/index.ts:489 and src/cli/index.ts:705restoreCodexCatalog() at src/codex/catalog/sync.ts:499-524 can propagate file-system errors, so an exception here skips the later revertSystemEnv()/stripGrokConfig() steps. Wrap both calls in the same best-effort try/catch used by syncCleanup().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/index.ts` around lines 213 - 243, Wrap both restoreCodexCatalog()
calls in the CLI flows, including the call sites near the shutdown and stop
handling paths, in best-effort try/catch blocks like syncCleanup(). Ensure any
exception is handled without aborting subsequent revertSystemEnv() and
stripGrokConfig() cleanup steps, while preserving the existing cleanup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/guides/web-dashboard.md`:
- Around line 62-66: Update the documentation around the Sub-agent delegation
picker and OpenCodex multi-agent guidance to limit the exact model and
reasoning-effort guarantee to the built-in guidance. State that custom
injectionPrompt content replaces the built-in guidance and must include the
{{model}} and/or {{effort}} placeholders when those overrides are required.

In `@gui/src/pages/use-dashboard-data.ts`:
- Around line 346-374: Update saveInjection to refresh the injection option
lists after a successful save: re-fetch /api/injection-model and apply its full
response, including injectionEfforts and injectionAvailable, rather than only
normalizing the PUT response. Preserve the existing committed UI state on
failure and always clear injectionSaving.

In `@gui/src/stop-proxy.ts`:
- Around line 12-16: Remove the hardcoded English fallback from failureMessage
and route the default stop-failure message through the existing i18n flow.
Update requestProxyStop or its App.tsx handleStop caller to supply a
t()-translated fallback, while preserving payload.message and payload.error
precedence and the existing HTTP status context.
- Around line 23-39: Update requestProxyStop to enforce a finite timeout around
the fetchFn call, using an AbortController or equivalent AbortSignal and
ensuring the timer is cleared after completion. Treat timeout-induced aborts
like other request errors by returning { accepted: true }, while preserving
existing response validation and cleanup behavior.

In `@gui/tests/app-stop.test.ts`:
- Around line 30-38: Guard both indexOf results in the test before slicing the
App.tsx source: assert that the "const handleStop" and "const brand" markers are
found and ordered correctly, then derive handler with slice. Keep the existing
handler-content assertions unchanged so the test fails loudly when either
boundary is missing or misplaced.

In `@src/cli/index.ts`:
- Around line 711-715: Update the restore/eject forward-switch handling around
stripGrokConfig so a failed result sets process.exitCode to a nonzero value
before logging, matching handleStop’s stopFailed behavior. Add a regression test
in the restore/eject CLI tests covering the !g.ok path and asserting the command
exits unsuccessfully.

In `@src/codex/subagent-defaults.ts`:
- Around line 461-474: Update the ownership check around TARGET_KEYS so
model/effort is evaluated as an atomic pair whenever defaults is non-null:
inspect both target definitions regardless of which keys are in desired, and add
conflicts when either existing key is user-owned. Preserve the existing no-op
return and conflict reporting so inject.ts warns whenever the pair cannot be
managed consistently.

In `@src/config.ts`:
- Around line 862-872: Update validFileConfigDiagnostics to avoid reassigning
its config parameter: compute the disabled reason from the original config
first, bind normalizeNativeSubagentSync(config, rawParsed) to a new local, and
use that local for subsequent configPlaceholderWarnings and related diagnostics.
Preserve the existing ordering so syncDisabledReason remains available for the
ignored-warning message.

In `@src/server/management/agent-settings-routes.ts`:
- Line 183: Extract the shared trim-aware predicate subagentDefaultSyncEffective
in src/config.ts using the syncCodexSubagentDefaults and injectionModel fields,
then replace the duplicated Boolean checks in this route’s GET/PUT response and
guard logic with it. Update configuredManagedSubagentDefaults in
src/codex/inject.ts to reuse the same predicate, preserving the intended
behavior that whitespace-only injectionModel values disable sync.
- Around line 247-257: Move the model-clearing reset block above the
supported-effort validation in the route handling update, so clearing nextModel
also clears nextEffort and nextSyncCodexSubagentDefaults before the guard
evaluates them. Preserve the existing validation for non-cleared models and add
a regression test covering PUT with model null, inherited
syncCodexSubagentDefaults true, and an unsupported injectionEffort.

In `@structure/05_gui-and-management-api.md`:
- Line 23: Update the Subagents row documenting PUT /api/injection-model to
state that contradictory cross-field combinations are rejected with HTTP 400:
enabling syncCodexSubagentDefaults requires an injection model and a supported
Codex reasoning injectionEffort. Also document that clearing the model
force-clears injectionEffort and syncCodexSubagentDefaults, matching the
enforcement in the agent-settings route.

In `@tests/codex-inject-integration.test.ts`:
- Around line 221-238: Extend the conflict scenarios in the tests around
“kept-user-base-url” and the external “model_provider” case to read back
config.toml after runInject. Assert that default_subagent_model and
default_subagent_reasoning_effort remain absent or unchanged when
nativeSubagentDefaultsWarning indicates routing ownership blocked injection,
while preserving the existing warning and success assertions. Confirm the inject
branch only writes these defaults when routing injection is allowed.

---

Outside diff comments:
In `@src/cli/index.ts`:
- Around line 213-243: Wrap both restoreCodexCatalog() calls in the CLI flows,
including the call sites near the shutdown and stop handling paths, in
best-effort try/catch blocks like syncCleanup(). Ensure any exception is handled
without aborting subsequent revertSystemEnv() and stripGrokConfig() cleanup
steps, while preserving the existing cleanup behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1b2605ce-6a31-4382-ab9e-417e8da31f75

📥 Commits

Reviewing files that changed from the base of the PR and between 911373d and c233079.

📒 Files selected for processing (55)
  • docs-site/src/content/docs/guides/codex-app-models.md
  • docs-site/src/content/docs/guides/sub-agent-surface.md
  • docs-site/src/content/docs/guides/web-dashboard.md
  • docs-site/src/content/docs/ja/guides/codex-app-models.md
  • docs-site/src/content/docs/ja/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ja/guides/web-dashboard.md
  • docs-site/src/content/docs/ja/reference/configuration.md
  • docs-site/src/content/docs/ko/guides/codex-app-models.md
  • docs-site/src/content/docs/ko/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ko/guides/web-dashboard.md
  • docs-site/src/content/docs/ko/reference/configuration.md
  • docs-site/src/content/docs/reference/configuration.md
  • docs-site/src/content/docs/ru/guides/codex-app-models.md
  • docs-site/src/content/docs/ru/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ru/guides/web-dashboard.md
  • docs-site/src/content/docs/ru/reference/configuration.md
  • docs-site/src/content/docs/zh-cn/guides/codex-app-models.md
  • docs-site/src/content/docs/zh-cn/guides/sub-agent-surface.md
  • docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
  • docs-site/src/content/docs/zh-cn/reference/configuration.md
  • gui/src/App.tsx
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/dashboard-core-poll.ts
  • gui/src/pages/dashboard-overview-sections.tsx
  • gui/src/pages/dashboard-shared.ts
  • gui/src/pages/use-dashboard-data.ts
  • gui/src/stop-proxy.ts
  • gui/tests/app-stop.test.ts
  • gui/tests/dashboard-contracts.test.ts
  • gui/tests/multi-agent-guidance.test.tsx
  • src/cli/index.ts
  • src/codex/inject.ts
  • src/codex/subagent-defaults.ts
  • src/codex/sync.ts
  • src/config.ts
  • src/server/management/agent-settings-routes.ts
  • src/server/management/config-routes.ts
  • src/types.ts
  • structure/02_config-and-codex-home.md
  • structure/03_catalog-and-subagents.md
  • structure/05_gui-and-management-api.md
  • tests/cli-restore-back.test.ts
  • tests/codex-inject-integration.test.ts
  • tests/codex-inject.test.ts
  • tests/codex-journal.test.ts
  • tests/codex-sync-api.test.ts
  • tests/config.test.ts
  • tests/grok-lifecycle.test.ts
  • tests/injection-model-api.test.ts
  • tests/subagent-defaults.test.ts

Comment on lines 62 to +66
The Dashboard's **Sub-agent delegation** picker stores `injectionModel` and, optionally,
`injectionEffort`. On a v1 turn, opencodex injects guidance telling the parent agent which exact
model and reasoning effort to pass to `spawn_agent`. Choosing a model enables that guidance at any
parent reasoning effort; clearing the model also clears the stored effort.
`injectionEffort`. **OpenCodex multi-agent guidance** independently controls the delegation
instructions that use those values. On eligible v2 turns, that guidance tells the parent
agent which exact model and reasoning effort to pass to `spawn_agent`; clearing the model also clears
the stored effort.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Qualify the spawn-agent guarantee when custom prompts are used.

This wording implies every guidance variant passes the exact model and effort to spawn_agent, but injectionPrompt can replace the built-in guidance entirely. A custom prompt without {{model}} or {{effort}} will not necessarily provide those overrides. Clarify that this guarantee applies to built-in guidance, and document that custom prompts must include the placeholders when needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/guides/web-dashboard.md` around lines 62 - 66,
Update the documentation around the Sub-agent delegation picker and OpenCodex
multi-agent guidance to limit the exact model and reasoning-effort guarantee to
the built-in guidance. State that custom injectionPrompt content replaces the
built-in guidance and must include the {{model}} and/or {{effort}} placeholders
when those overrides are required.

Source: Path instructions

Comment on lines +346 to +374
const saveInjection = async (patch: {
multiAgentGuidanceEnabled?: boolean;
syncCodexSubagentDefaults?: boolean;
model?: string | null;
effort?: string | null;
}) => {
if (injectionSaving) return;
setInjectionSaving(true);
try {
const res = await fetch(`${apiBase}/api/injection-model`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
const data = await requireJson<{
multiAgentGuidanceEnabled?: boolean;
syncCodexSubagentDefaults?: boolean;
model?: string | null;
effort?: string | null;
}>(res);
const normalized = normalizeInjectionSelection(data);
setMultiAgentGuidanceEnabled(normalized.multiAgentGuidanceEnabled);
setSyncCodexSubagentDefaults(normalized.syncCodexSubagentDefaults);
setInjectionModel(normalized.injectionModel);
setInjectionEffort(normalized.injectionEffort);
} catch { /* keep the last committed UI state */ }
finally { setInjectionSaving(false); }
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether PUT /api/injection-model returns efforts/available in its response body.
fd -e ts agent-settings-routes.ts | xargs rg -n -A 30 "PUT|injection-model"

Repository: lidge-jun/opencodex

Length of output: 17099


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== server PUT /api/injection-model =="
sed -n '191,290p' src/server/management/agent-settings-routes.ts | cat -n

echo
echo "== frontend saveInjection and nearby state =="
sed -n '330,390p' gui/src/pages/use-dashboard-data.ts | cat -n

echo
echo "== dashboard core poll / injection refresh =="
fd -e ts dashboard-core-poll.ts gui/src | while read -r f; do
  echo "--- $f ---"
  sed -n '200,260p' "$f" | cat -n
done

Repository: lidge-jun/opencodex

Length of output: 10771


Refresh the injection option lists after a save. saveInjection only applies multiAgentGuidanceEnabled/syncCodexSubagentDefaults/model/effort, but PUT /api/injection-model in src/server/management/agent-settings-routes.ts returns no efforts or available payload. That leaves injectionEfforts and injectionAvailable stale until the next 5s poll, so the effort dropdown can briefly show choices for the previous model after a model change. Re-fetch /api/injection-model after a successful save, or have the PUT handler return the full GET shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/use-dashboard-data.ts` around lines 346 - 374, Update
saveInjection to refresh the injection option lists after a successful save:
re-fetch /api/injection-model and apply its full response, including
injectionEfforts and injectionAvailable, rather than only normalizing the PUT
response. Preserve the existing committed UI state on failure and always clear
injectionSaving.

Comment thread gui/src/stop-proxy.ts
Comment on lines +12 to +16
function failureMessage(payload: ProxyStopPayload | null, status: number): string {
if (typeof payload?.message === "string" && payload.message.trim()) return payload.message;
if (typeof payload?.error === "string" && payload.error.trim()) return payload.error;
return `Failed to stop proxy (HTTP ${status}).`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded fallback string bypasses i18n.

Failed to stop proxy (HTTP ${status}). is a new user-visible string shown via alert() (see App.tsx handleStop), but it's a raw literal rather than routed through the app's t()/locale system used everywhere else in App.tsx (t("dash.stopConfirm"), t("dash.stop"), etc.).

♻️ Suggested direction

Pass a translated fallback into requestProxyStop/failureMessage (or have the caller substitute a t()-sourced default when outcome.message is absent) instead of hardcoding English text in this module.

As per path instructions, "Check that GUI state changes stay consistent with the management API responses and that user-visible strings go through the i18n locale files rather than hardcoded text."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/stop-proxy.ts` around lines 12 - 16, Remove the hardcoded English
fallback from failureMessage and route the default stop-failure message through
the existing i18n flow. Update requestProxyStop or its App.tsx handleStop caller
to supply a t()-translated fallback, while preserving payload.message and
payload.error precedence and the existing HTTP status context.

Source: Path instructions

Comment thread gui/src/stop-proxy.ts
Comment on lines +23 to +39
export async function requestProxyStop(
apiBase: string,
fetchFn: typeof fetch = fetch,
): Promise<ProxyStopOutcome> {
let response: Response;
try {
response = await fetchFn(`${apiBase}/api/stop`, { method: "POST" });
} catch {
return { accepted: true };
}

const payload = await response.json().catch(() => null) as ProxyStopPayload | null;
if (!response.ok || payload?.success === false) {
return { accepted: false, message: failureMessage(payload, response.status) };
}
return { accepted: true };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout guards the stop request.

fetchFn is awaited with no AbortSignal/timeout. The function only resolves {accepted:true} on a thrown error (e.g., connection reset) or {accepted:false} on a received response — if the network stalls without ever throwing or resolving, App.tsx's stopping state (and the disabled Stop button) never recovers until the browser's own long default socket timeout kicks in.

♻️ Suggested fix
 export async function requestProxyStop(
   apiBase: string,
   fetchFn: typeof fetch = fetch,
 ): Promise<ProxyStopOutcome> {
   let response: Response;
   try {
-    response = await fetchFn(`${apiBase}/api/stop`, { method: "POST" });
+    response = await fetchFn(`${apiBase}/api/stop`, { method: "POST", signal: AbortSignal.timeout(10_000) });
   } catch {
     return { accepted: true };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function requestProxyStop(
apiBase: string,
fetchFn: typeof fetch = fetch,
): Promise<ProxyStopOutcome> {
let response: Response;
try {
response = await fetchFn(`${apiBase}/api/stop`, { method: "POST" });
} catch {
return { accepted: true };
}
const payload = await response.json().catch(() => null) as ProxyStopPayload | null;
if (!response.ok || payload?.success === false) {
return { accepted: false, message: failureMessage(payload, response.status) };
}
return { accepted: true };
}
export async function requestProxyStop(
apiBase: string,
fetchFn: typeof fetch = fetch,
): Promise<ProxyStopOutcome> {
let response: Response;
try {
response = await fetchFn(`${apiBase}/api/stop`, { method: "POST", signal: AbortSignal.timeout(10_000) });
} catch {
return { accepted: true };
}
const payload = await response.json().catch(() => null) as ProxyStopPayload | null;
if (!response.ok || payload?.success === false) {
return { accepted: false, message: failureMessage(payload, response.status) };
}
return { accepted: true };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/stop-proxy.ts` around lines 23 - 39, Update requestProxyStop to
enforce a finite timeout around the fetchFn call, using an AbortController or
equivalent AbortSignal and ensuring the timer is cleared after completion. Treat
timeout-induced aborts like other request errors by returning { accepted: true
}, while preserving existing response validation and cleanup behavior.

Comment on lines +30 to +38
test("App clears stopping state and alerts for every rejected stop outcome", async () => {
const app = await Bun.file(new URL("../src/App.tsx", import.meta.url)).text();
const handler = app.slice(app.indexOf("const handleStop"), app.indexOf("const brand"));

expect(handler).toContain("await requestProxyStop(API_BASE)");
expect(handler).toContain("if (!outcome.accepted)");
expect(handler).toContain("setStopping(false)");
expect(handler).toContain("alert(outcome.message)");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the indexOf results before slicing.

app.indexOf("const handleStop") and app.indexOf("const brand") (Line 32) are used unguarded. If "const brand" disappears or moves before handleStop in App.tsx in a future refactor, indexOf returns -1 and .slice(start, -1) silently widens the extracted handler string to almost the entire file instead of failing loudly — the test would then keep passing without actually verifying that the assertions are scoped to handleStop, masking a real regression. gui/tests/dashboard-contracts.test.ts (Lines 18-19) already establishes the safer pattern of asserting toBeGreaterThan(-1) before slicing; this test should follow suit.

🧪 Proposed fix
-    const handler = app.slice(app.indexOf("const handleStop"), app.indexOf("const brand"));
+    const handlerStart = app.indexOf("const handleStop");
+    const handlerEnd = app.indexOf("const brand");
+    expect(handlerStart).toBeGreaterThan(-1);
+    expect(handlerEnd).toBeGreaterThan(handlerStart);
+    const handler = app.slice(handlerStart, handlerEnd);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("App clears stopping state and alerts for every rejected stop outcome", async () => {
const app = await Bun.file(new URL("../src/App.tsx", import.meta.url)).text();
const handler = app.slice(app.indexOf("const handleStop"), app.indexOf("const brand"));
expect(handler).toContain("await requestProxyStop(API_BASE)");
expect(handler).toContain("if (!outcome.accepted)");
expect(handler).toContain("setStopping(false)");
expect(handler).toContain("alert(outcome.message)");
});
test("App clears stopping state and alerts for every rejected stop outcome", async () => {
const app = await Bun.file(new URL("../src/App.tsx", import.meta.url)).text();
const handlerStart = app.indexOf("const handleStop");
const handlerEnd = app.indexOf("const brand");
expect(handlerStart).toBeGreaterThan(-1);
expect(handlerEnd).toBeGreaterThan(handlerStart);
const handler = app.slice(handlerStart, handlerEnd);
expect(handler).toContain("await requestProxyStop(API_BASE)");
expect(handler).toContain("if (!outcome.accepted)");
expect(handler).toContain("setStopping(false)");
expect(handler).toContain("alert(outcome.message)");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/tests/app-stop.test.ts` around lines 30 - 38, Guard both indexOf results
in the test before slicing the App.tsx source: assert that the "const
handleStop" and "const brand" markers are found and ordered correctly, then
derive handler with slice. Keep the existing handler-content assertions
unchanged so the test fails loudly when either boundary is missing or misplaced.

Comment thread src/config.ts
Comment on lines +862 to +872
function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
// An unsafe hand-edited opt-in is disabled in memory instead of rejecting
// the entire config, which would hide unrelated providers/accounts. The next
// ordinary save persists the normalized absence.
const syncDisabledReason = nativeSubagentSyncDisabledReason(config, rawParsed);
config = normalizeNativeSubagentSync(config, rawParsed);
const warnings = configPlaceholderWarnings(config);
warnings.push(...malformedNativeSubagentFields(rawParsed).map(malformedNativeSubagentFieldWarning));
if (syncDisabledReason) {
warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid reassigning the config parameter; the correctness here depends on statement order.

Line 866 must run before line 867: nativeSubagentSyncDisabledReason returns null as soon as config.syncCodexSubagentDefaults !== true, and normalizeNativeSubagentSync deletes exactly that key. Swap the two lines during any future refactor and syncDisabledReason becomes permanently null, silently dropping the syncCodexSubagentDefaults ignored: … warning from /api/* diagnostics — with no type error and no compile failure to catch it.

Binding the normalized value to a new local makes the dependency explicit and removes the ordering trap.

♻️ Proposed refactor
-function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
+function validFileConfigDiagnostics(parsedConfig: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
   // An unsafe hand-edited opt-in is disabled in memory instead of rejecting
   // the entire config, which would hide unrelated providers/accounts. The next
   // ordinary save persists the normalized absence.
-  const syncDisabledReason = nativeSubagentSyncDisabledReason(config, rawParsed);
-  config = normalizeNativeSubagentSync(config, rawParsed);
+  // Read the reason BEFORE normalizing: normalization deletes the very key the
+  // reason check keys on.
+  const syncDisabledReason = nativeSubagentSyncDisabledReason(parsedConfig, rawParsed);
+  const config = normalizeNativeSubagentSync(parsedConfig, rawParsed);
   const warnings = configPlaceholderWarnings(config);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
// An unsafe hand-edited opt-in is disabled in memory instead of rejecting
// the entire config, which would hide unrelated providers/accounts. The next
// ordinary save persists the normalized absence.
const syncDisabledReason = nativeSubagentSyncDisabledReason(config, rawParsed);
config = normalizeNativeSubagentSync(config, rawParsed);
const warnings = configPlaceholderWarnings(config);
warnings.push(...malformedNativeSubagentFields(rawParsed).map(malformedNativeSubagentFieldWarning));
if (syncDisabledReason) {
warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
}
function validFileConfigDiagnostics(parsedConfig: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
// An unsafe hand-edited opt-in is disabled in memory instead of rejecting
// the entire config, which would hide unrelated providers/accounts. The next
// ordinary save persists the normalized absence.
// Read the reason BEFORE normalizing: normalization deletes the very key the
// reason check keys on.
const syncDisabledReason = nativeSubagentSyncDisabledReason(parsedConfig, rawParsed);
const config = normalizeNativeSubagentSync(parsedConfig, rawParsed);
const warnings = configPlaceholderWarnings(config);
warnings.push(...malformedNativeSubagentFields(rawParsed).map(malformedNativeSubagentFieldWarning));
if (syncDisabledReason) {
warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 862 - 872, Update validFileConfigDiagnostics to
avoid reassigning its config parameter: compute the disabled reason from the
original config first, bind normalizeNativeSubagentSync(config, rawParsed) to a
new local, and use that local for subsequent configPlaceholderWarnings and
related diagnostics. Preserve the existing ordering so syncDisabledReason
remains available for the ignored-warning message.

)));
return jsonResponse({
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
syncCodexSubagentDefaults: config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The "sync is effectively on" rule is now duplicated four times across three files — extract one predicate.

config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel) appears at line 183, again at lines 211-212, and again at line 273 in this file; configuredManagedSubagentDefaults in src/codex/inject.ts (lines 78) encodes the same rule with !config.injectionModel, and nativeSubagentSyncDisabledReason in src/config.ts (lines 771-774) encodes a stricter variant using injectionModel?.trim().

That last one is already a divergence: this route treats a whitespace-only injectionModel as truthy (Boolean(" ") is true), while src/config.ts treats it as blank and disables sync. A hand-edited "injectionModel": " " therefore makes GET /api/injection-model report syncCodexSubagentDefaults: true while loadConfig() strips the flag and injection never writes the defaults. Note the PUT path at line 247 already uses nextModel?.trim(), so the trim rule is the intended one — GET/PUT responses are the outliers.

♻️ Proposed fix: one exported predicate, trim-consistent

Add next to the other config helpers (e.g. src/config.ts):

/** Single source of truth for "native sub-agent default sync is effectively on". */
export function subagentDefaultSyncEffective(
  config: Pick<OcxConfig, "syncCodexSubagentDefaults" | "injectionModel">,
): boolean {
  return config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
}

Then in this file:

-      syncCodexSubagentDefaults: config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel),
+      syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),

applied at lines 183, 211-212, and 273, and reuse it for the guard in configuredManagedSubagentDefaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/management/agent-settings-routes.ts` at line 183, Extract the
shared trim-aware predicate subagentDefaultSyncEffective in src/config.ts using
the syncCodexSubagentDefaults and injectionModel fields, then replace the
duplicated Boolean checks in this route’s GET/PUT response and guard logic with
it. Update configuredManagedSubagentDefaults in src/codex/inject.ts to reuse the
same predicate, preserving the intended behavior that whitespace-only
injectionModel values disable sync.

Comment on lines +247 to +257
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
}
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
}
// Clearing the model always clears model-dependent settings.
if (!nextModel) {
nextEffort = undefined;
nextSyncCodexSubagentDefaults = false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reorder the model-clearing reset above the effort guard, or clearing the model can be permanently rejected.

Line 250 gates on the effective nextSyncCodexSubagentDefaults (which inherits true from config), while the reset at lines 254-257 that clears nextEffort runs afterwards. With a persisted syncCodexSubagentDefaults: true and a stale unsupported config.injectionEffort (hand-edited, or written by an older build before the ladder changed), a PUT {"model": null} takes this path:

  1. line 230 → nextModel = undefined
  2. line 247 → skipped (body.syncCodexSubagentDefaults is absent, not true)
  3. line 250 → nextSyncCodexSubagentDefaults is still inherited true, nextEffort is the stale invalid value → 400
  4. lines 254-257 never run

The user cannot clear the injection model through the API at all, even though clearing it is exactly the action that would resolve the invalid state. loadConfig()'s normalization (src/config.ts lines 776-778) hides this on a fresh process, but the route mutates the shared in-memory config, so it persists for the life of the running proxy.

Applying the model-cleared reset first makes the guard consistent with the state actually about to be saved:

🐛 Proposed fix
     if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
       return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
     }
+    // Clearing the model always clears model-dependent settings — do this before
+    // the effort guard so a stale invalid effort cannot block the clear itself.
+    if (!nextModel) {
+      nextEffort = undefined;
+      nextSyncCodexSubagentDefaults = false;
+    }
     if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
       return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
     }
-    // Clearing the model always clears model-dependent settings.
-    if (!nextModel) {
-      nextEffort = undefined;
-      nextSyncCodexSubagentDefaults = false;
-    }

A regression test for PUT {"model": null} against a config with syncCodexSubagentDefaults: true plus an unsupported injectionEffort would lock this in alongside the existing cases in tests/injection-model-api.test.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
}
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
}
// Clearing the model always clears model-dependent settings.
if (!nextModel) {
nextEffort = undefined;
nextSyncCodexSubagentDefaults = false;
}
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
}
// Clearing the model always clears model-dependent settings — do this before
// the effort guard so a stale invalid effort cannot block the clear itself.
if (!nextModel) {
nextEffort = undefined;
nextSyncCodexSubagentDefaults = false;
}
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/management/agent-settings-routes.ts` around lines 247 - 257, Move
the model-clearing reset block above the supported-effort validation in the
route handling update, so clearing nextModel also clears nextEffort and
nextSyncCodexSubagentDefaults before the guard evaluates them. Preserve the
existing validation for non-cleared models and add a regression test covering
PUT with model null, inherited syncCodexSubagentDefaults true, and an
unsupported injectionEffort.

| Key providers | Expose API-key provider presets for setup and dashboard flows. Multi-key pool per key-auth provider: `GET /api/providers/keys`, `POST /api/providers/keys`, `PUT /api/providers/keys/active`, `PUT /api/providers/keys/alias`, `DELETE /api/providers/keys` masked list, add (upsert + activate), switch, rename, and remove keys. `provider.apiKey` always mirrors the active pool entry so routing stays single-key. |
| OpenAI account mode | Report one OpenAI Codex card with Pool/Direct controls and one API-key card. Mode PATCH persists live without restart or catalog identity changes; Pool owns account/quota controls and Direct uses caller/main login only. Main-account DTOs report real credential presence and terminal `needsReauth` state instead of treating missing/invalid native auth as an unknown quota. |
| Subagents | Read/write the featured `subagentModels` list capped at five ids. |
| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the PUT rejection rules, matching how the /api/v2 row states its contract.

The row describes what the endpoint manages but not what it refuses. src/server/management/agent-settings-routes.ts enforces two hard 400s (lines 247-252): enabling syncCodexSubagentDefaults without an injection model, and enabling it while the effective injectionEffort is not a supported Codex reasoning effort. It also force-clears the effort and the sync flag when the model is cleared (lines 254-257). The neighbouring /api/v2 row already sets the precedent by stating "contradictory mode/flag pairs are rejected before writes", so a maintainer reading only this row would not expect the cross-field validation.

📝 Suggested addition
-| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. |
+| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. PUT rejects enabling the opt-in without an injection model or with an unsupported reasoning effort, and clearing the model clears both the effort and the opt-in. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. |
| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. PUT rejects enabling the opt-in without an injection model or with an unsupported reasoning effort, and clearing the model clears both the effort and the opt-in. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@structure/05_gui-and-management-api.md` at line 23, Update the Subagents row
documenting PUT /api/injection-model to state that contradictory cross-field
combinations are rejected with HTTP 400: enabling syncCodexSubagentDefaults
requires an injection model and a supported Codex reasoning injectionEffort.
Also document that clearing the model force-clears injectionEffort and
syncCodexSubagentDefaults, matching the enforcement in the agent-settings route.

Comment on lines 221 to +238
test("kept-user-base-url: reports routing NOT injected and leaves the user's override alone", () => {
writeFileSync(join(codexHome, "config.toml"), [
'openai_base_url = "https://my-own-gateway.example/v1"',
'model = "gpt-5.5"',
"",
].join("\n"), "utf8");

const r = runInject(codexHome, ocxHome);
const r = runInject(codexHome, ocxHome, JSON.stringify({
syncCodexSubagentDefaults: true,
injectionModel: "gpt-5.6-sol",
injectionEffort: "high",
}));
expect(r.status).toBe(0);
const result = JSON.parse(r.stdout);
expect(result.success).toBe(true);
expect(result.message).toContain("routing NOT injected");
expect(result.message).not.toContain("All models now route through opencodex proxy");
expect(result.nativeSubagentDefaultsWarning).toContain("user-owned root openai_base_url");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Coverage gap: warning is asserted, but the actual config.toml write isn't verified in the same conflict scenarios.

Both extended tests confirm result.nativeSubagentDefaultsWarning fires when routing can't be injected (user-owned openai_base_url at Line 238, external model_provider at Line 300), but neither reads back config.toml to confirm default_subagent_model/default_subagent_reasoning_effort were not actually written in these branches. Since the whole point of this warning is "routing-ownership blocks injection" (per PR objectives), a regression where the native defaults get written anyway (pointing at a model that isn't actually reachable through the unrouted provider) would silently slip past this suite — only the warning text is checked, not the resulting file state.

🧪 Suggested additional assertions
     expect(result.nativeSubagentDefaultsWarning).toContain("user-owned root openai_base_url");
+    const injected = readFileSync(join(codexHome, "config.toml"), "utf8");
+    expect(injected).not.toContain("default_subagent_model");
+    expect(injected).not.toContain("default_subagent_reasoning_effort");

(equivalent addition for the external-provider test around Line 300)

Since this depends on how src/codex/inject.ts actually branches when nativeSubagentDefaultsWarning is set (not included in this review batch), please confirm the intended behavior before treating this purely as a test-only gap.

Also applies to: 288-300

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/codex-inject-integration.test.ts` around lines 221 - 238, Extend the
conflict scenarios in the tests around “kept-user-base-url” and the external
“model_provider” case to read back config.toml after runInject. Assert that
default_subagent_model and default_subagent_reasoning_effort remain absent or
unchanged when nativeSubagentDefaultsWarning indicates routing ownership blocked
injection, while preserving the existing warning and success assertions. Confirm
the inject branch only writes these defaults when routing injection is allowed.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Request changes ÔÇö one API contract bug in model-clear ordering, plus required CI has not actually run (fork action_required). Feature direction and ownership/TOML design look solid; not merge-ready yet.

Summary

Opt-in syncCodexSubagentDefaults is well scoped: marker-owned TOML edits, journal scrub, structured warnings, docs/structure/GUI/API coverage, and focused tests. Base is correctly dev. The main code defect is that the documented "clearing model always clears effort + sync" path can 400 before the clear side effects run.

Blocking findings

  1. PUT /api/injection-model model-clear can 400 before clearing ÔÇö src/server/management/agent-settings-routes.ts ~L250ÔÇô257
    • Failure mode: With in-memory syncCodexSubagentDefaults: true, a model set, and a non-Codex injectionEffort, PUT { "model": null } hits the sync+effort gate at L250ÔÇô252 and returns 400 syncCodexSubagentDefaults requires a supported Codex reasoning effort. The clear block at L254ÔÇô257 (which would drop effort and sync) never runs, so the documented contract ("clearing model always clears both the effort and native-default opt-in") is violated. Load-time normalization usually strips sync for bad effort, so this is easiest to hit via hand-built/makeConfig state or any path that skips normalizeNativeSubagentSync ÔÇö but the control-flow order is still wrong relative to the adjacent comment and docs.
    • Fix: Move the if (!nextModel) { nextEffort = undefined; nextSyncCodexSubagentDefaults = false; } block above the sync/effort validation (or skip that gate when clearing the model). Add a regression test: sync on + unsupported inherited effort + PUT { model: null } ÔåÆ 200 and all three cleared.

Merge conflicts (required)

Prior review omitted this; current GitHub state is mergeable: CONFLICTING / DIRTY against dev. Author must rebase or merge dev and resolve the conflicts in this PR before merge (same bar as the other required quality items below).

Content conflicts in:

  1. src/cli/index.ts
  2. tests/cli-restore-back.test.ts

Additional required quality changes (fix in this PR before merge)

These are needed for merge quality (same spirit as soft-nits elevation on related work) ÔÇö not optional polish or follow-up PRs.

  1. Docs curl enables sync without a model ÔÇö docs-site/src/content/docs/guides/sub-agent-surface.md ~L94ÔÇô100 (and ja/ko/ru/zh-cn clones). Snippet is only {"syncCodexSubagentDefaults": true}, which alone is 400 (requires an injection model). Prose below is correct; fix the example payload (include model) or mark it as "after model is set". Sync locales.
  2. Duplicated "sync effectively on" predicate ÔÇö config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel) appears in GET/PUT responses and inject paths. Extract one helper (whitespace-aware if you want GET/PUT and inject to agree on trim). Consolidate in this PR so response and inject paths cannot drift.
  3. Lifecycle / stop-proxy blast radius ÔÇö exit-code and stop-proxy typing changes are adjacent to restore reporting; keep them only if intentional for this feature, otherwise split out of this PR so review scope stays clear. (CodeRabbit's Grok-strip exit-code / atomic ownership / GUI timeout claims look overstated against this diff ÔÇö ownership early-return + tests cover the pair; scope correctly limits Grok best-effort.)

Standards / Spec / Security notes

  • Standards: Base dev OK. Bun-native, privacy, and no auth/credential/workflow surface touched. Behavior changes have focused tests; docs + structure updated. Docs example vs API remains a required standards fix before merge.
  • Spec: PR-body requirements largely met (opt-in, ownership/TOML, journal scrub, API/Dashboard/i18n/docs, warnings, nonzero restore/sync reporting). Spec gap is the model-clear ordering above. Scope creep is small and justified.
  • Security / privacy: No secret logging, no OAuth/token/workflow changes. Config.toml mutation remains marker-scoped and fail-closed on ambiguous ownership ÔÇö good. Not a security-boundary PR under MAINTAINERS.md, but config-file mutation still deserves careful review (this PR's ownership model looks sound).

CI / process gates

  • Target branch: dev ÔÇö correct.
  • Required checks: Cross-platform CI, Service lifecycle, and React Doctor are all action_required (fork PR from wonsh42/opencodex) ÔÇö they have not executed. Maintainer must approve workflows and wait for green before merge. Author-reported local bun run test / typecheck / privacy:scan are not a substitute.
  • Maintainer approval still required per MAINTAINERS.md; this review is not an approve.

After fixes

Once all blocking + required quality changes above are addressed (and conflicts/rebase done), mark this PR Ready for review (leave draft only until then). Then ping for re-review / CI.

lidge-jun added a commit that referenced this pull request Jul 27, 2026
Two read-only audit lenses returned 30 contradictions; the verified ones are
applied here and recorded in 006_corrections.md.

Factual corrections:
- claude-code#1124 closed 2025-05-16, not 2025-08-10
- 13 open enhancement issues, not 11
- #529 is merged, so issue #42 phase 2 is done
- #418 has no PR behind it; it was miscounted as in-flight
- #528 does not contain #424's current head, so it needs a rebase
- #527's retarget is manual and independent of #526

Framing corrections:
- stale-needs-info.yml is absent from the default branch, so bundle C had no
  real deadline
- the debug switch #543's reporter asked for already exists
- whether #545 is our defect is answerable from code, not owner judgment
- #491 is an OAuth credential change, so it is a security boundary rather
  than a warm-up; #533 and #447 keep their security classification too

Structure: bundle G added for roadmap honesty, bundle F rescoped to the
security boundary, #498 split out of bundle B, cycle order rebuilt, and the
loop archetype corrected to decision-elicitation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants