Skip to content

fix(deepseek): advertise the ladder each V4 model actually honors (#1057) - #1069

Merged
lidge-jun merged 5 commits into
devfrom
codex/1057-deepseek-effort-ladder
Aug 6, 2026
Merged

fix(deepseek): advertise the ladder each V4 model actually honors (#1057)#1069
lidge-jun merged 5 commits into
devfrom
codex/1057-deepseek-effort-ladder

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

DeepSeek documents a low / high / max ladder, and the two V4 models resolve it differently. From the official thinking-mode table (EN and zh-cn agree, re-verified 2026-08-06 immediately before this change):

requested deepseek-v4-flash deepseek-v4-pro
low low high
high high high
xhigh high max
max max max

We advertised ["high","xhigh","max"] for both and mapped low -> high for both. So native low was unreachable, and xhigh sat in the picker as though it were a tier rather than an alias for something else.

Both the ladder and the map are now per model:

  • Flash advertises low/high/max — it honors all three.
  • Pro advertises high/max. It must not offer a low tier the vendor silently upgrades to high; that would be this same defect wearing a different value, and the reporter's requested low -> low is correct for Flash but wrong for Pro.
  • xhigh stays in both wire maps so existing requests and saved configs keep working. It is simply no longer advertised as native, which is what the issue actually asks for.

medium has no row in the vendor table. Mapping it to high is our own compatibility choice for clients that only speak the OpenAI ladder, and the code now says so.

Seven provider entries share these constants (opencode-go, orcarouter, deepseek, volcengine-coding-plan, both Alibaba token plans, opencode-free), so all seven move together. The Flash-vs-Pro split is a substring test on the model id — correct for every id shipped today, including deepseek/deepseek-v4-pro and deepseek-v4-flash-free, and exactly the kind of thing that misfires on a future name. A parity test therefore enumerates every provider/model pair with its expected ladder and alias mapping, so a misclassification fails loudly with the offending id named.

Three existing tests changed. All three asserted the defect itself (the advertised ["high","xhigh","max"] and a shared xhigh -> max); the alias assertions that were testing compatibility rather than the defect are untouched.

Closes #1057.

Verification

$ bun run typecheck
(clean)

$ bun test tests/provider-registry-parity.test.ts tests/volcengine-providers.test.ts tests/opencode-go-deepseek.test.ts
 53 pass, 0 fail

$ bun test <provider/vision/catalog/reasoning scope, 11 files>
 179 pass, 0 fail

$ bun run privacy:scan
Privacy scan passed

Red-green: disabling the Flash classification (isDeepseekFlashModel forced to false) fails 2 tests, including the new enumeration; restoring it returns 34 pass / 0 fail.

Full suite on this branch: 9015 pass / 1 fail. The one failure is jawcode-metadata-sync, which fails identically on origin/dev and is unrelated to this change.

Source: https://api-docs.deepseek.com/guides/thinking_mode/

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

    • DeepSeek V4 Flash and Pro models now offer model-specific reasoning options and compatibility mappings.
    • Volcengine Coding Plan models now apply model-appropriate reasoning levels, including improved handling of low and xhigh.
  • Bug Fixes

    • Corrected reasoning-effort translation for DeepSeek and Volcengine models.
  • Documentation

    • Added detailed plans and research covering model capabilities, vision support, native-profile testing, and startup app-server behavior.
  • Tests

    • Expanded provider parity and reasoning-mode coverage across supported models.

Four defects from the triage, each with a pre-written diff-level layer doc.

Two research findings changed the plan before any code was written. DeepSeek's
official table maps xhigh to max on Pro but to high on Flash, so the single
shared mapping constant our registry uses cannot be corrected with one edit.
And a live probe of all eight Zen free models found two that accept images -
including the one a community report claimed refuses them - so the obvious
blanket classification would have silently destroyed working vision input.
Seven blockers, all accepted. Two would have shipped wrong code.

Pro must not advertise a low tier DeepSeek silently upgrades to high - the
earlier draft fixed the reported defect and reintroduced it at another value.
And the startup warning could not observe its own trigger: the sync seam
discards its result, and a 5s state cache can serve a pre-write fresh reading
back to the post-write check.

Also corrects the stack shape. The 010/020 registry edits are two lines apart
on the same provider object, not 1300 apart as claimed, while 030 and 040
share nothing and go straight to dev.
The migration would have handed Pro back the low tier the registry change
removes - the same defect, reintroduced through the upgrade path for existing
users only. Migration is now per model like the registry, and the test is that
a migrated config equals a fresh install.

Layer 040's helper now takes the real CodexAppServerProcessIo seam, which
already carries kill, listSnapshots, readStartMs and catalogMtimeMs. The draft
promised injection while showing a helper with nothing to inject into, awaited
a synchronous function, and read a field named state as status.

030 now awaits the SIGKILL escalation before throwing, so the caller's
exitCode assertion is not racing the reap.
Two startup write sites would each call the helper and warn twice. handleStart
now owns the single call: startServer returns whether its cache invalidation
wrote, syncCodexOnStartIfEnabled returns its typed result, neither warns, and
handleStart ORs the flags and warns once after both.

The ordering is correctness, not tidiness - warning after the first write reads
a catalog mtime the second write is about to move.
)

DeepSeek documents low/high/max, and the two V4 models resolve it differently:
requested xhigh becomes max on Pro but high on Flash, and requested low is
honored natively on Flash while Pro upgrades it to high.

We advertised [high, xhigh, max] for both and mapped low to high for both. So
native low was unreachable, and xhigh sat in the picker as though it were a
tier rather than an alias for something else.

Both are now per model. Flash advertises low/high/max, Pro advertises high/max
- Pro must not offer a low tier the vendor silently bills as high, which would
be this same defect wearing a different value. xhigh stays in both wire maps so
existing requests and saved configs keep working; it is simply no longer
advertised as native.

Seven provider entries share these constants, so a parity test now enumerates
every provider/model pair and its expected ladder and alias mapping. The
Flash-vs-Pro split is a substring test on the model id, correct for every id
shipped today and exactly the kind of thing that misfires on a future name.

Source: api-docs.deepseek.com/guides/thinking_mode, EN and zh-cn agree,
re-verified 2026-08-06 immediately before this change.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deterministic PR hygiene checks passed.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Bug-fix stack

Layer / File(s) Summary
Stack scope and upstream findings
devlog/_plan/260805_bug_fix_stack/000_plan.md, devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md
Documents issue dependencies, scope exclusions, DeepSeek mappings, Zen modality findings, and shared acceptance criteria.
DeepSeek model-specific reasoning plan
devlog/_plan/260805_bug_fix_stack/010_deepseek_ladder.md
Defines separate Flash and Pro ladders, model classification, configuration migration, and validation requirements.
DeepSeek registry integration
src/providers/registry.ts
Applies model-specific reasoning efforts and wire mappings across DeepSeek provider entries.
DeepSeek reasoning validation
tests/opencode-go-deepseek.test.ts, tests/provider-registry-parity.test.ts, tests/volcengine-providers.test.ts
Verifies Flash and Pro ladders, aliases, and provider-specific wire formats.
OpenCode Zen modality plan
devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md, devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md, devlog/_plan/260805_bug_fix_stack/020_zen_text_only.md
Records image-capability probes and plans explicit text-only model classification.
Native-profile harness plan
devlog/_plan/260805_bug_fix_stack/030_native_profile_harness.md
Plans atomic JSON publication, parse-aware polling, bounded teardown, and deterministic tests.
Startup app-server warning plan
devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md
Plans stale-state detection after startup writes without restarting or terminating app-server processes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated devlog plans for Zen modality, native-profile harness, and startup app-server fixes beyond issue #1057. Remove the unrelated devlog plan files or move them to separate pull requests aligned with their respective issues.
✅ 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 clearly identifies the DeepSeek V4 reasoning-ladder fix, which is the main code change.
Linked Issues check ✅ Passed The registry and tests implement model-specific DeepSeek V4 ladders and compatibility mappings required by issue #1057.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/1057-deepseek-effort-ladder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the bug Something isn't working label Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🤖 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 `@devlog/_plan/260805_bug_fix_stack/000_plan.md`:
- Line 45: Add the text language tag to the opening fenced code blocks at
devlog/_plan/260805_bug_fix_stack/000_plan.md lines 45-45 and
devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md lines 50-50 by
changing each fence to ```text.
- Around line 76-82: Update the DeepSeek requirement in the plan to state the
definitive contract: Flash advertises low/high/max, while Pro advertises
high/max, with compatibility mappings kept model-specific for both low and
xhigh. Remove the provisional confirmation language and ensure the requirement
prevents Pro from advertising or mapping low as supported.

In `@devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md`:
- Around line 8-9: Correct the observation date in the source evidence entry to
the actual collection date, or explicitly mark it as planned rather than
confirmed. Update the timing language in the corresponding present-tense
statements at lines 34-38 so it consistently reflects the corrected evidence
status.

In `@devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md`:
- Around line 15-20: Make the probe command runnable by defining the model
variable or adding the intended model loop before the curl request, and replace
the abbreviated PNG data URL with the complete valid 1x1 payload. If the snippet
is intentionally pseudocode, label it accordingly and provide a separate command
with a defined model and complete image data.
- Line 6: Update the paragraph in 002_zen_modality_probe.md so the reference is
written as “issue `#1043`” or otherwise escapes the hash, keeping it as paragraph
text and preventing Markdown from interpreting it as an ATX heading.

In `@devlog/_plan/260805_bug_fix_stack/010_deepseek_ladder.md`:
- Around line 38-43: Replace the future verification date with the actual date
the vendor check was completed: update
devlog/_plan/260805_bug_fix_stack/010_deepseek_ladder.md lines 38-43, 71-74, and
170-174; the DeepSeek registry entry in src/providers/registry.ts lines 352-375;
the date assertion in tests/opencode-go-deepseek.test.ts lines 108-110; and both
parity references in tests/provider-registry-parity.test.ts lines 111-114 and
945-948. Keep the surrounding ladder documentation, registry metadata, and test
expectations unchanged.

In `@devlog/_plan/260805_bug_fix_stack/020_zen_text_only.md`:
- Around line 131-142: Extend the acceptance tests to cover both opencode-zen
and opencode-free entries: assert all six measured IDs are present and
mimo-v2.5-free plus longcat-2.0-free are absent for each registry entry. Add
activation coverage for both provider routes, or explicitly verify they consume
the same derived configuration; do not rely on separate key-provider derivation
as coverage for opencode-free.

In `@devlog/_plan/260805_bug_fix_stack/030_native_profile_harness.md`:
- Around line 114-117: Update the waitForJson partial-write test to synchronize
on an observed parse failure before replacing the file atomically. Add a barrier
or injectable read hook that confirms the waiter has attempted to parse the
partial "{" content and failed, then perform the replacement and assert the
parsed object is returned; do not use a fixed sleep for synchronization.
- Around line 144-150: Update the “Accept criteria” section to require an
injected-stall test proving the teardown timeout fires; remove the option to
substitute an unexercised-branch statement. Allow that statement only for the
final post-SIGKILL timeout branch.
- Around line 124-126: Add blank lines immediately before and after the nested
TypeScript code fence in the documented OCX_TEST_STALL_ON_STOP snippet,
preserving the snippet’s contents and formatting otherwise.
- Around line 131-135: Update the cleanup assertion in the startup-child
termination flow to verify the subprocess has exited and that either
child.exitCode or child.signalCode is non-null, allowing intentional SIGKILL
termination to satisfy the check. Preserve the existing awaited SIGKILL cleanup
and rejection-message assertions.

In `@devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md`:
- Around line 50-67: Update handleStart and its production warning call to pass
a non-default CodexAppServerProcessIo seam into
warnIfStaleCodexAppServersAfterStartupWrite, ensuring
collectCodexAppServerCatalogState bypasses the five-second memo after the
confirmed startup write. Reuse the existing process I/O seam rather than
invalidating the cache or changing unrelated startup behavior.
- Around line 158-184: Expand the startup test coverage around handleStart to
exercise the cacheWritten || syncWritten gate: add separate cases for cache-only
stale startup, sync-only stale startup, both writes, and neither write. Assert
exactly one warning for each case where either path writes, and zero warnings
when neither path writes, using the injected log and existing
CodexAppServerProcessIo seam.
- Around line 97-106: Update CodexStartupSync and defaultStartupSync to return
the structured CodexSyncResult from syncModelsToCodex instead of a boolean. In
syncCodexOnStartIfEnabled, preserve best-effort startup behavior and the
existing catch, but return a distinct skipped, successful-write, or failed
result so callers can evaluate the catalogWritten || cacheSynced gate.
- Around line 170-187: Update handleStart and the startup sync path so the
test-seam lifetime/dependency object is propagated into
afterCatalogWriteHandleAppServers. For the warning-only startup run, pass
restart: false and the injected CodexAppServerProcessIo implementation,
including listSnapshots, readStartMs, catalogMtimeMs, now, and a kill function
that fails if called; preserve the existing sync and sync-cache handling while
ensuring no path invokes restartCodexAppServers or process.kill.
🪄 Autofix

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: 83fa74b3-5edf-42f6-8b55-9a6e4c2174c5

📥 Commits

Reviewing files that changed from the base of the PR and between 0e92714 and 45ac9eb.

📒 Files selected for processing (11)
  • devlog/_plan/260805_bug_fix_stack/000_plan.md
  • devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md
  • devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md
  • devlog/_plan/260805_bug_fix_stack/010_deepseek_ladder.md
  • devlog/_plan/260805_bug_fix_stack/020_zen_text_only.md
  • devlog/_plan/260805_bug_fix_stack/030_native_profile_harness.md
  • devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md
  • src/providers/registry.ts
  • tests/opencode-go-deepseek.test.ts
  • tests/provider-registry-parity.test.ts
  • tests/volcengine-providers.test.ts

nothing and cost two retargets after the parents land. They go straight to `dev`
as independent PRs, which `AGENTS.md` permits alongside stacked children.

```

Copy link
Copy Markdown
Contributor

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

Add language tags to both fenced blocks.

markdownlint-cli2 reports MD040 for both blocks. Add text to each opening fence.

  • devlog/_plan/260805_bug_fix_stack/000_plan.md#L45-L45: change the opening fence to ```text.
  • devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md#L50-L50: change the opening fence to ```text.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 45-45: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 2 files
  • devlog/_plan/260805_bug_fix_stack/000_plan.md#L45-L45 (this comment)
  • devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md#L50-L50
🤖 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 `@devlog/_plan/260805_bug_fix_stack/000_plan.md` at line 45, Add the text
language tag to the opening fenced code blocks at
devlog/_plan/260805_bug_fix_stack/000_plan.md lines 45-45 and
devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md lines 50-50 by
changing each fence to ```text.

Source: Linters/SAST tools

Comment on lines +76 to +82
**#1057: the shared mapping table may be wrong per model.** DeepSeek's official
thinking-mode docs give a native ladder of `low / high / max`, which matches the
reporter. But the same table maps requested `xhigh` differently per model —
`xhigh -> max` for `deepseek-v4-pro` and `xhigh -> high` for `deepseek-v4-flash`.
The code currently applies one shared map to both. A confirmation lane is running
against the official table before this layer is written; if the per-model
difference holds, the fix is not a one-line constant change.

Copy link
Copy Markdown
Contributor

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

Make the DeepSeek requirement definitive and complete.

Lines 80-82 still say that confirmation is running and identify only an xhigh split. devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md Lines 23-32 also confirm a low split: Flash maps low to low, while Pro maps low to high.

State the final contract here: Flash advertises low/high/max; Pro advertises high/max; compatibility mappings remain model-specific. Otherwise, a later patch can fix only xhigh and still advertise low for Pro. The official table shows these model-specific mappings. (api-docs.deepseek.com)

🤖 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 `@devlog/_plan/260805_bug_fix_stack/000_plan.md` around lines 76 - 82, Update
the DeepSeek requirement in the plan to state the definitive contract: Flash
advertises low/high/max, while Pro advertises high/max, with compatibility
mappings kept model-specific for both low and xhigh. Remove the provisional
confirmation language and ensure the requirement prevents Pro from advertising
or mapping low as supported.

Comment on lines +8 to +9
Source: [api-docs.deepseek.com/guides/thinking_mode](https://api-docs.deepseek.com/guides/thinking_mode/),
observed 2026-08-06. The Chinese mirror agrees verbatim.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the evidence date before treating this source as confirmed.

Line 9 records August 6, 2026, but the current review date is August 5, 2026. The document cannot report an observation from tomorrow. Lines 34-38 also use present-tense timing based on that record.

Replace the date with the actual collection date, or mark the observation as planned. Align the timing language before using this document as upstream evidence.

Also applies to: 34-38

🤖 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 `@devlog/_plan/260805_bug_fix_stack/001_upstream_evidence.md` around lines 8 -
9, Correct the observation date in the source evidence entry to the actual
collection date, or explicitly mark it as planned rather than confirmed. Update
the timing language in the corresponding present-tense statements at lines 34-38
so it consistently reflects the corrected evidence status.

The search lane could not classify these models: the official docs publish no
modality, and `GET /v1/models` returns only `id`, `object`, `created`,
`owned_by` — no capability field at all. That absence *is* the root cause of
#1043, so it could not also serve as its evidence.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep #1043 inside the paragraph.

Line [6] starts with #1043. Markdownlint reports MD018 for this malformed ATX heading. Write issue #1043`` or escape the hash so the text remains part of the paragraph.

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 6-6: No space after hash on atx style heading

(MD018, no-missing-space-atx)

🤖 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 `@devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md` at line 6,
Update the paragraph in 002_zen_modality_probe.md so the reference is written as
“issue `#1043`” or otherwise escapes the hash, keeping it as paragraph text and
preventing Markdown from interpreting it as an ATX heading.

Source: Linters/SAST tools

Comment on lines +15 to +20
```bash
IMG='{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAA...ErkJggg=="}}'
curl -s https://opencode.ai/zen/v1/chat/completions \
-H "content-type: application/json" -H "x-opencode-client: desktop" \
-d "{\"model\":\"$m\",\"max_tokens\":8,\"messages\":[{\"role\":\"user\",
\"content\":[{\"type\":\"text\",\"text\":\"what is in this image\"},$IMG]}]}"

Copy link
Copy Markdown
Contributor

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

Make the probe command reproducible.

The command uses an undefined $m, and the PNG data URL contains .... As written, it sends an empty model name and invalid image data. Define the model before the request or add the model loop. Replace the abbreviated data URL with the complete 1x1 PNG payload. If this is only pseudocode, label it as such and provide a separate runnable command.

🤖 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 `@devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md` around lines 15
- 20, Make the probe command runnable by defining the model variable or adding
the intended model loop before the curl request, and replace the abbreviated PNG
data URL with the complete valid 1x1 payload. If the snippet is intentionally
pseudocode, label it accordingly and provide a separate command with a defined
model and complete image data.

Comment on lines +144 to +150
## Accept criteria

- Teardown bounded with a kill fallback; no unbounded `await child.exited`.
- The settled-file read proves parseable JSON, not existence.
- The child publishes that file atomically.
- A deterministic test for the parse race; an injected-stall test for the timeout,
or an explicit statement that the branch is unexercised.

Copy link
Copy Markdown
Contributor

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

Require the injected timeout test in the acceptance criteria.

Lines 141-142 state that the bounded wait is unfalsifiable without the injected-stall case, but line 150 permits an explicit statement instead. That exception allows the hang fix to merge without proof that the deadline fires. Keep the injected-stall test mandatory. Limit the “unexercised” note to the final post-SIGKILL timeout branch.

🤖 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 `@devlog/_plan/260805_bug_fix_stack/030_native_profile_harness.md` around lines
144 - 150, Update the “Accept criteria” section to require an injected-stall
test proving the teardown timeout fires; remove the option to substitute an
unexercised-branch statement. Allow that statement only for the final
post-SIGKILL timeout branch.

Comment on lines +50 to +67
export function warnIfStaleCodexAppServersAfterStartupWrite(
opts: {
log?: Pick<Console, "error">;
io?: CodexAppServerProcessIo; // the real seam, src/codex/app-server-processes.ts:79-89
} = {},
): { warned: boolean } {
try {
// Pass `io` through: tests inject listSnapshots/readStartMs/catalogMtimeMs/now,
// and supplying any field also bypasses the 5s memo (`fullyDefault` at :580),
// which is what stops a pre-write `fresh` reading from masking this check.
const status = collectCodexAppServerCatalogState(opts.io ?? {});
if (status.state !== "stale") return { warned: false };
(opts.log ?? console).error(formatStaleCodexAppServerWarning(status.processes));
return { warned: true };
} catch {
return { warned: false }; // startup sync is best-effort; never fail boot
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Pass a non-default process I/O object to the production warning call.

collectCodexAppServerCatalogState uses the memo when every CodexAppServerProcessIo field is defaulted, as shown in src/codex/app-server-processes.ts, Lines [576-585]. The production call in Lines [146-147] invokes warnIfStaleCodexAppServersAfterStartupWrite() without options. It can therefore reuse a pre-write fresh result for five seconds.

This violates the cache regression requirement in Lines [177-179] and acceptance criterion in Line [205]. Thread the existing I/O seam into handleStart and pass a non-empty production seam, or explicitly invalidate the cache after the final confirmed write.

Also applies to: 140-151

🤖 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 `@devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md` around lines 50
- 67, Update handleStart and its production warning call to pass a non-default
CodexAppServerProcessIo seam into warnIfStaleCodexAppServersAfterStartupWrite,
ensuring collectCodexAppServerCatalogState bypasses the five-second memo after
the confirmed startup write. Reuse the existing process I/O seam rather than
invalidating the cache or changing unrelated startup behavior.

Comment on lines +97 to +106
**The sync result is currently thrown away.** `defaultStartupSync` returns
`syncModelsToCodex(port)` as `Promise<unknown>`, and `syncCodexOnStartIfEnabled`
does `await sync(port).catch(() => {})` then returns a bare `boolean` meaning
"the integration was enabled", not "a write happened". So the `catalogWritten ||
cacheSynced` gate this layer depends on **cannot be evaluated** as the code
stands.

Fix the seam first: type `CodexStartupSync` to return the sync result, keep the
`.catch` (startup must stay best-effort), and let the caller distinguish
*skipped* / *ran and wrote* / *ran and failed*. Only then is the write gate real.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 '\b(syncModelsToCodex|CodexStartupSync|syncCodexOnStartIfEnabled)\b' src tests

Repository: lidge-jun/opencodex

Length of output: 50375


No change needed. syncModelsToCodex already returns a structured CodexSyncResult; CodexStartupSync can be typed to return that result, preserving the existing .catch(() => {}) behavior while making write/write-failure states available to callers.

🤖 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 `@devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md` around lines 97
- 106, Update CodexStartupSync and defaultStartupSync to return the structured
CodexSyncResult from syncModelsToCodex instead of a boolean. In
syncCodexOnStartIfEnabled, preserve best-effort startup behavior and the
existing catch, but return a distinct skipped, successful-write, or failed
result so callers can evaluate the catalogWritten || cacheSynced gate.

Comment on lines +158 to +184
**Test:** `tests/codex-desired-state.test.ts` — a startup where both the cache
invalidation and the sync report a write emits exactly one warning; asserted on
the injected `log.error` call count, not on the message.

## Tests

No test currently combines startup sync with app-server handling — verified by an
exhaustive read-only scan across `origin/dev`'s `tests/` for files mentioning both
`syncCodexOnStartIfEnabled|handleStart` and
`afterCatalogWriteHandleAppServers|collectCodexAppServerCatalogState`. Result:
empty.

**Add**, faking both boundaries through the existing `CodexAppServerProcessIo`
seam (`io.listSnapshots`, `io.readStartMs`, `io.catalogMtimeMs`, `io.now`) so no
new injection point is invented:

1. the warning runs only when the sync reports a write — requires the typed
result from the seam fix above;
2. stale warns; `fresh`, `not_running`, and `unknown` do not;
3. **pre-write `fresh` then post-write `stale` still warns** — this is the
cache-masking regression, and it is the test that would have caught the bug the
audit found;
4. **an injected `kill` that fails the test if called**, proving no startup path
can reach `restartCodexAppServers()`;
5. discovery throwing still resolves startup successfully;
6. a startup hitting both write paths warns exactly once.
Asserted through `handleStart`'s injected log, per the ownership rule above.

Copy link
Copy Markdown
Contributor

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

Test both sides of the startup write gate.

Line [174] says the warning runs only when the sync reports a write. Lines [146-147] define the gate as cacheWritten || syncWritten, and Lines [118-125] include cache-only startup writes in the defect scope.

Add separate cases for cache-only stale startup, sync-only stale startup, both writes, and neither write. Assert one warning for each write case and zero warnings when neither path writes.

🤖 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 `@devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md` around lines 158
- 184, Expand the startup test coverage around handleStart to exercise the
cacheWritten || syncWritten gate: add separate cases for cache-only stale
startup, sync-only stale startup, both writes, and neither write. Assert exactly
one warning for each case where either path writes, and zero warnings when
neither path writes, using the injected log and existing CodexAppServerProcessIo
seam.

Comment on lines +170 to +187
**Add**, faking both boundaries through the existing `CodexAppServerProcessIo`
seam (`io.listSnapshots`, `io.readStartMs`, `io.catalogMtimeMs`, `io.now`) so no
new injection point is invented:

1. the warning runs only when the sync reports a write — requires the typed
result from the seam fix above;
2. stale warns; `fresh`, `not_running`, and `unknown` do not;
3. **pre-write `fresh` then post-write `stale` still warns** — this is the
cache-masking regression, and it is the test that would have caught the bug the
audit found;
4. **an injected `kill` that fails the test if called**, proving no startup path
can reach `restartCodexAppServers()`;
5. discovery throwing still resolves startup successfully;
6. a startup hitting both write paths warns exactly once.
Asserted through `handleStart`'s injected log, per the ownership rule above.

For (4), inject `io.kill` and fail the test if it is ever called. The seam exists
at `src/codex/app-server-processes.ts:84`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 10 '\b(handleStart|warnIfStaleCodexAppServersAfterStartupWrite|CodexAppServerProcessIo)\b' src tests

Repository: lidge-jun/opencodex

Length of output: 23641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files mentioning syncCodexOnStartIfEnabled / afterCatalogWrite / handleStart =="
rg -n -C 8 '\bsyncCodexOnStartIfEnabled\b|\bafterCatalogWrite|warnIfStale|handleStart\(' src tests devlog || true

echo
echo "== src/cli/index.ts handleStart relevant section =="
sed -n '170,280p' src/cli/index.ts

echo
echo "== targeted static call graph for collectCodexAppServerCatalogState/restartCodexAppServers/AfterCatalogWrite =="
python3 - <<'PY'
from pathlib import Path
for p in Path('src').rglob('*.ts'):
    s=p.read_text(errors='ignore')
    if 'collectCodexAppServerCatalogState' in s or 'warnIfStaleCodexAppServers' in s or 'AfterCatalogWriteAppServerOptions' in s or 'restartCodexAppServers' in s:
        print(f'--- {p}')
        lines=s.splitlines()
        for i,l in enumerate(lines,1):
            if any(x in l for x in ['collectCodexAppServerCatalogState', 'warnIfStaleCodexAppServersAfterStartupWrite', 'function warnIfStale', 'AfterCatalogWriteAppServerOptions', 'restartCodexAppServers']) or any(x in l for x in ['from "../codex/app-server-processes"','function warn', 'await warn', 'collectCodexAppServerCatalogState', 'warnIfStaleCodexAppServers']):
                start=max(1,i-4); end=min(len(lines),i+6)
                for j in range(start,end+1):
                    print(f'{j:4}: {lines[j-1]}')
PY

Repository: lidge-jun/opencodex

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md tail =="
sed -n '80,210p' devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md

echo
echo "== afterCatalogWriteHandleAppServers implementation =="
sed -n '712,770p' src/codex/app-server-processes.ts

echo
echo "== handleStart calls involving syncCodexOnStartIfEnabled / catalog writes / afterCatalogWrite =="
sed -n '300,330p' src/cli/index.ts
sed -n '848,862p' src/cli/index.ts
sed -n '877,881p' src/cli/index.ts

echo
echo "== source-level contract check =="
python3 - <<'PY'
from pathlib import Path
cli = Path("src/cli/index.ts").read_text()
plan = Path("devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md").read_text()
for needle in ["syncCodexOnStartIfEnabled", "handleStart({", "syncCodexOnStartIfEnabled("]:
    print(f"{needle}: {cli.index(needle) if needle in cli else 'ABSSENT'}")
handler_start=cli.index("afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });")
handler_start2=cli.index("afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });", handler_start+1)
print(f"handler option keys near sync: {cli[handler_start-80:handler_start+60]}")
print(f"handler option keys near sync-cache: {cli[handler_start2-80:handler_start2+60]}")
print(f"plan mentions startSafe handler/option: {'startup-safe' in plan and 'startup: restart: false' in plan}")
print(f"plan mentions start hook call: {'afterCatalogWriteHandleAppServers({ restart: false, log: console, io:' in plan or 'handleStart' in plan[plan.index('# Add'):plan.index('For (4)')] if '# Add' in plan else 'NO_ADD_HEAD'}")
PY

Repository: lidge-jun/opencodex

Length of output: 12167


Wire handleStart to the stale-app-server test seams.

handleStart() calls syncCodexOnStartIfEnabled(port, config) and then the existing sync / sync-cache case calls afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console }) with no injectable process I/O. As written, afterCatalogWriteHandleAppServers() can still call process.kill if restartCodex becomes true, so the injected kill guard cannot prove the startup path is safe. Add a test-seam lifetime/dependency object so the warning-only startup run passes restart: false plus a CodexAppServerProcessIo with injected listSnapshots, readStartMs, catalogMtimeMs, now, and a failing kill.

🤖 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 `@devlog/_plan/260805_bug_fix_stack/040_startup_app_server.md` around lines 170
- 187, Update handleStart and the startup sync path so the test-seam
lifetime/dependency object is propagated into afterCatalogWriteHandleAppServers.
For the warning-only startup run, pass restart: false and the injected
CodexAppServerProcessIo implementation, including listSnapshots, readStartMs,
catalogMtimeMs, now, and a kill function that fails if called; preserve the
existing sync and sync-cache handling while ensuring no path invokes
restartCodexAppServers or process.kill.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45ac9ebf87

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/providers/registry.ts
*/
modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate stale DeepSeek reasoning metadata

This updates only the registry defaults, but existing users who added deepseek, opencode-go, Volcengine Coding Plan, etc. before this release have the old modelReasoningEfforts and modelReasoningEffortMap persisted in config.json from providerConfigSeed() / key login. routedProviderConfig() then lets those saved per-model arrays replace the new registry arrays, and the saved nested map keys override the new map keys, so an upgraded deepseek-v4-flash provider still advertises xhigh and still sends low -> high / xhigh -> max. Please add a config/backfill migration or make these built-in provider facts canonical so the fix applies to already-installed providers, not only freshly added ones.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Comment thread src/providers/registry.ts
*/
modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate stale DeepSeek reasoning metadata

This updates only the registry defaults, but existing users who added deepseek, opencode-go, Volcengine Coding Plan, etc. before this release have the old modelReasoningEfforts and modelReasoningEffortMap persisted in config.json from providerConfigSeed() / key login. routedProviderConfig() then lets those saved per-model arrays replace the new registry arrays, and the saved nested map keys override the new map keys, so an upgraded deepseek-v4-flash provider still advertises xhigh and still sends low -> high / xhigh -> max. Please add a config/backfill migration or make these built-in provider facts canonical so the fix applies to already-installed providers, not only freshly added ones.

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun merged commit cdb963e into dev Aug 6, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant