-
Notifications
You must be signed in to change notification settings - Fork 856
fix: stabilize dev head after the 260818 merge train #2026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 010 — WP-V stabilization audit (post-interruption) | ||
|
|
||
| Context: prior session (thread 01a0138d) was interrupted mid-campaign by a codex | ||
| runtime error; user reports "too much merged too fast" and asks for a full | ||
| main..dev merge appropriateness audit + CI + lidge suite before continuing. | ||
|
|
||
| Range: origin/main (e97fb2621, v2.25.0) .. origin/dev (aaf04690e), 125 commits. | ||
| A new push to dev re-opens ALL THREE verifiers (CI, lidge suite, whole-delta diff). | ||
|
|
||
| ## Audit lanes (parallel, gpt-5.6-sol medium, read-only) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Move remediation out of the read-only audit lanes. Line 10 declares the lanes read-only, but Line 48 requires a fix-forward or targeted revert “in B” and re-verification “in C”. Lines 15-20 define B and C as audit lanes, not writable remediation phases. Define the remediation owner and phase explicitly. After remediation, record a new immutable Also applies to: 15-20, 48-48 🤖 Prompt for AI Agents |
||
|
|
||
| - Lane A — campaign land-* merges: #2015(1800) #2016(2007) #2017(1990) | ||
| #2018(1889+1883-followup) #2020(1896) #2021(1932). Check: matrix verdict match, | ||
| rebase correctness (vpr-* merge shape), tests present, no scope creep. | ||
| - Lane B — campaign batch merges: 1991 1931 1912 1859 1847 1845 (merge), | ||
| 1935 1725 1851 (squash), 1883 (squash, workflow security). Check: matrix match, | ||
| squash-vs-merge shape as prescribed, workflow security for 1883. | ||
| - Lane C — pre-campaign merges on dev: 1928 1941 2005 1904 1965 1893 1949 | ||
| 1944-1947 1998 1997 + docs 2004-2014 + b5a98d690 release-audit fixes. | ||
| Check: each is a reviewed, coherent landing; docs merges are docs-only. | ||
| - Lane D — whole-delta security/semantic scan: git diff origin/main..origin/dev | ||
| focused on src/ high-risk surfaces, explicitly including: | ||
| .github/scripts/install-copilot-cli.sh + run-copilot-inference.cjs (supply chain, | ||
| #1883), src/lib/windows-service-wrappers.ts + windows-atomic-replace.ts | ||
| (privileged kill/replace), src/server/management/system-routes.ts + shared.ts | ||
| (management API), src/codex/auth-api.ts + plan-from-token.ts (#1998/#1932 WHAM | ||
| 401 gating), src/oauth/google-antigravity.ts (#1889), src/lib/redact.ts, | ||
| scripts/build-release-changelog.ts (#1847), MiniMax loopback pin e9d879b34. | ||
|
Comment on lines
+12
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Require an exact coverage manifest for commits and paths. The lane lists name selected merge or pull-request identifiers and selected high-risk paths. They do not prove that all 125 commits and all changed paths received a verdict. An unlisted commit or path can escape review while the acceptance criterion still claims exact coverage. Generate manifests from Also applies to: 44-45 🧰 Tools🪛 LanguageTool[uncategorized] ~23-~23: The official name of this software platform is spelled with a capital “H”. (GITHUB) [grammar] ~28-~28: Ensure spelling is correct (QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1) 🪛 markdownlint-cli2 (0.23.2)[warning] 12-12: Spaces inside emphasis markers (MD037, no-space-in-emphasis) 🤖 Prompt for AI Agents |
||
|
|
||
| ## Verifiers (PLAN-VERIFIER-REAL-01) | ||
|
|
||
| - gh run watch 32130622133 (Cross-platform CI on aaf04690e) — observes dev head; running now. | ||
| - ssh lidge full suite (typecheck + bun test --isolate tests + privacy:scan) in a | ||
| DEDICATED git worktree pinned at aaf04690e (~/.wpv-suite-aaf04690e). The shared | ||
| ~/Developer/opencodex checkout is owned by a concurrent session (split-wp1b) and | ||
| was swapped mid-run — the first suite attempt (ssh session 27978) is VOID. | ||
| - Failure baseline: any lidge failure is classified by bisect-attribution into | ||
| e97fb2621..aaf04690e (ours) vs reproduction at 0f5ccf9aa pre-campaign tip | ||
| (preexisting). No judgment-call classifications. | ||
|
Comment on lines
+37
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Define The plan does not specify the bisect command, the test oracle, the treatment of flaky or environment failures, or the evidence for the first bad commit. This leaves the Specify the exact suite command. Run it at 🤖 Prompt for AI Agents |
||
| - Local dirty worktree (#1748 delta) is stashed out of scope for WP-V; it belongs to wp6. | ||
|
|
||
| ## Accept criteria | ||
|
|
||
| - Every merge group has a verdict: OK / SUSPECT(reason) / REGRESSION(evidence); | ||
| coverage list is exact over all 125 commits (incl. docs #2004). | ||
| - CI conclusion recorded for exact SHA aaf04690e (or successor if new pushes land). | ||
| - lidge suite exit codes recorded; failures classified ours-vs-preexisting. | ||
| - Any REGRESSION gets fix-forward or targeted revert in B, re-verified in C. | ||
|
|
||
| Out of scope: wp6-wp11 work (later cycles). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,8 +221,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| expect(container.querySelector(".badge.badge-amber")?.textContent).toContain("Discovery failed"); | ||
| expect(container.textContent).not.toContain("Not selected"); | ||
|
|
||
| await act(async () => buttonText("Context windows").click()); | ||
| const contextDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const contextDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
|
Comment on lines
+224
to
+225
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- changed test references ---'
sed -n '200,240p;265,315p;315,335p;345,365p;375,390p;415,430p;435,465p' gui/tests/models-empty-provider.test.tsx
printf '%s\n' '--- production dialog and button ---'
rg -n -C 8 'contextSettings|contextSettingsTitle|aria-label=.*Custom|role="dialog"' gui/src/pages/Models.tsx gui/src/i18n/en.ts
printf '%s\n' '--- test translation setup and related helpers ---'
sed -n '1,120p' gui/tests/models-empty-provider.test.tsx
rg -n -C 5 'buttonText|i18n|translator|t\\(' gui/tests/models-empty-provider.test.tsx gui/tests | head -240Repository: lidge-jun/opencodex Length of output: 24166 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- locale provider implementation ---'
rg -n -C 8 'export.*LanguageProvider|function LanguageProvider|const LanguageProvider|useTranslation|export.*useTranslation|function useTranslation' gui/src/i18n
printf '%s\n' '--- all relevant test literals and selectors ---'
rg -n -C 2 'Custom windows|Apply|Cancel|Context windows|aria-label=.*Custom' gui/tests/models-empty-provider.test.tsx
printf '%s\n' '--- test file diff summary and changed lines ---'
git diff --stat -- gui/tests/models-empty-provider.test.tsx
git diff --unified=1 -- gui/tests/models-empty-provider.test.tsx | sed -n '1,260p'Repository: lidge-jun/opencodex Length of output: 10025 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- locale detection and translation API ---'
cat -n gui/src/i18n/provider.tsx
cat -n gui/src/i18n/shared.ts | sed -n '1,180p'
printf '%s\n' '--- context settings translations ---'
rg -n 'contextSettings|contextSaved|contextUnchanged|contextSaveFailed' gui/src/i18n
printf '%s\n' '--- test locale state and LanguageProvider usage ---'
rg -n -C 4 'localStorage|locale|LanguageProvider|setLocale|navigator.language' gui/tests/models-empty-provider.test.tsx gui/src/i18nRepository: lidge-jun/opencodex Length of output: 36082 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
models = Path("gui/src/pages/Models.tsx").read_text()
english = Path("gui/src/i18n/en.ts").read_text()
test = Path("gui/tests/models-empty-provider.test.tsx").read_text()
button = re.search(r'<button\b(?:(?!</button>).)*>\{t\("([^"]+)"\)\}</button>', models, re.S)
dialog = re.search(r'role="dialog"\s+aria-modal="true"\s+aria-label=\{t\("([^"]+)"\)\}', models)
en_values = dict(re.findall(r'"([^"]*context[^"]*)":\s*"([^"]*)"', english, re.I))
literal_count = test.count("Custom windows")
print("button_translation_key:", button.group(1) if button else None)
print("dialog_translation_key:", dialog.group(1) if dialog else None)
print("english_button_value:", en_values.get("models.contextSettings"))
print("english_title_value:", en_values.get("models.contextSettingsTitle"))
print("hardcoded_test_literal_count:", literal_count)
for locale_file in sorted(Path("gui/src/i18n").glob("*.ts")):
text = locale_file.read_text()
match = re.search(r'"models\.contextSettings":\s*"([^"]*)"', text)
if match:
print(f"{locale_file.name}: {match.group(1)}")
PYRepository: lidge-jun/opencodex Length of output: 541 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
source = Path("gui/src/pages/Models.tsx").read_text()
start = source.index("{contextModalProvider && (")
end = source.index("{customModalOpen && (", start)
block = source[start:end]
keys = re.findall(r'aria-label=\{t\("([^"]+)"\)\}', block)
print("context_modal_aria_label_keys:", keys)
print("context_modal_contains_context_settings_key:", "models.contextSettings" in keys)
print("test_uses_english_context_settings_literal:", '"Custom windows"' in Path("gui/tests/models-empty-provider.test.tsx").read_text())
PYRepository: lidge-jun/opencodex Length of output: 330 Use the locale value for these selectors. The button and context dialog both use 🤖 Prompt for AI AgentsSource: Path instructions |
||
| const contextInputs = contextDialog.querySelectorAll<HTMLInputElement>("input"); | ||
| expect([...contextInputs].map(input => input.value)).toEqual(["256000", "64000"]); | ||
| const setValue = Object.getOwnPropertyDescriptor( | ||
|
|
@@ -275,10 +275,10 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| contextWindow: 350_000, | ||
| modelContextWindows: { "claude-opus": 100_000, "claude-sonnet": 80_000 }, | ||
| }); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); | ||
|
|
||
| await act(async () => buttonText("Context windows").click()); | ||
| const refreshFailureDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const refreshFailureDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| failCatalog = true; | ||
| // Make an actual edit. Apply now compares against the values the modal opened with, so a | ||
| // reopened-and-untouched dialog sends nothing — which would leave this case asserting the | ||
|
|
@@ -296,15 +296,15 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| }); | ||
| expect(contextBodies).toHaveLength(2); | ||
| expect(contextBodies.at(-1)).toEqual({ contextWindow: 360_000 }); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); | ||
| expect(container.textContent).toContain("Context windows updated"); | ||
| failCatalog = false; | ||
|
|
||
| // An edit that is typed and then restored is not a change — and neither is retyping the | ||
| // same number in a different shape. Comparing raw text instead of parsed values would | ||
| // treat "64,000" as an edit and stamp a stale number over whatever else moved. | ||
| await act(async () => buttonText("Context windows").click()); | ||
| const revertDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const revertDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| const revertInput = revertDialog.querySelectorAll<HTMLInputElement>("input.input")[0]!; | ||
| const openingValue = revertInput.value; | ||
| await act(async () => { | ||
|
|
@@ -320,10 +320,10 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| await new Promise(resolve => testWindow.setTimeout(resolve, 0)); | ||
| }); | ||
| expect(contextBodies).toHaveLength(2); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); | ||
|
|
||
| await act(async () => buttonText("Context windows").click()); | ||
| const reformatDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const reformatDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| const reformatInput = reformatDialog.querySelectorAll<HTMLInputElement>("input.input")[0]!; | ||
| const commaFormatted = reformatInput.value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); | ||
| await act(async () => { | ||
|
|
@@ -353,8 +353,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| // The poll has to actually run: mutating the mock alone leaves React's `groups` on the | ||
| // opening values, and then comparing drafts against LIVE state — the defect — would look | ||
| // identical to comparing against the snapshot. | ||
| await act(async () => buttonText("Context windows").click()); | ||
| const concurrentDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const concurrentDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| providerContextWindow = 300_000; | ||
| providerModelContextWindows = { ...providerModelContextWindows, "claude-opus": 96_000 }; | ||
| await act(async () => { poll(); await new Promise(resolve => testWindow.setTimeout(resolve, 0)); }); | ||
|
|
@@ -379,8 +379,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| // `groups` instead of the opening snapshot. The cases above cannot see that swap, because | ||
| // in each of them the user's value genuinely differs from both. This one does — the user | ||
| // touches a field and puts it back, while the server moves underneath. | ||
| await act(async () => buttonText("Context windows").click()); | ||
| const staleDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const staleDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| const staleDefaultInput = staleDialog.querySelectorAll<HTMLInputElement>("input.input")[0]!; | ||
| const staleOpeningDefault = staleDefaultInput.value; | ||
| await act(async () => { | ||
|
|
@@ -419,8 +419,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| // anyone whose config was hand-edited before the safe-integer bound existed. | ||
| providerContextWindow = 1e100; | ||
| await act(async () => { poll(); await new Promise(resolve => testWindow.setTimeout(resolve, 0)); }); | ||
| await act(async () => buttonText("Context windows").click()); | ||
| const unsafeDefaultDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const unsafeDefaultDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| await pickContextModel("claude-sonnet", unsafeDefaultDialog); | ||
| const unsafeSiblingInput = unsafeDefaultDialog.querySelectorAll<HTMLInputElement>("input.input")[1]!; | ||
| await act(async () => { | ||
|
|
@@ -438,8 +438,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| // `Number.isInteger(1e100)` is true, and the server rejects it. Accepting it in the form | ||
| // would turn a typo into a round-trip error instead of inline feedback. | ||
| const patchesBeforeUnsafe = contextBodies.length; | ||
| await act(async () => buttonText("Context windows").click()); | ||
| const unsafeDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Context windows"]')!; | ||
| await act(async () => buttonText("Custom windows").click()); | ||
| const unsafeDialog = container.querySelector<HTMLElement>('[role="dialog"][aria-label="Custom windows"]')!; | ||
| const unsafeInput = unsafeDialog.querySelectorAll<HTMLInputElement>("input.input")[0]!; | ||
| await act(async () => { | ||
| setValue.call(unsafeInput, "1e100"); | ||
|
|
@@ -454,7 +454,7 @@ test("Models page combines final visibility, atomic actions, discovery status, a | |
| // Relative, not absolute: an absolute count silently re-targets whenever a case is added | ||
| // above, and the property under test is "this Apply wrote nothing". | ||
| expect(contextBodies).toHaveLength(patchesBeforeUnsafe); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).not.toBeNull(); | ||
| expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).not.toBeNull(); | ||
| // The modal staying open is not the point — the user has to be TOLD why. Without this the | ||
| // test passes on a silent no-op that looks identical to a hang. | ||
| expect(unsafeDialog.textContent).toContain("Context windows must be positive whole numbers"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3669,7 +3669,12 @@ async function handleResponsesInner( | |
| }), | ||
| }); | ||
| } else { | ||
| upstreamResponse = await fetchWithTransientRetry( | ||
| // #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for | ||
| // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other | ||
| // adapters keep reset-only retry so combo failover still hops on the first 5xx | ||
| // instead of burning ~1.2s of same-target retries per hop. | ||
| const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry; | ||
| upstreamResponse = await fetchWithRetryPolicy( | ||
|
Comment on lines
+3672
to
+3677
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Add a focused regression test for both retry policies. The changed behavior must prove that direct Google requests retry transient 5xx responses, while other adapters use reset-only retry. Cover both the initial request and terminal continuation request. Record attempt counts and recovery labels with a replayable fetch stub. As per path instructions: a behavior change in Also applies to: 4107-4110 🤖 Prompt for AI AgentsSource: Path instructions |
||
| recovery => { | ||
| noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery); | ||
| return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({ | ||
|
|
@@ -4099,7 +4104,10 @@ async function handleResponsesInner( | |
| }), | ||
| }); | ||
| } | ||
| return await fetchWithTransientRetry( | ||
| // Same #1851 scope guard as the initial send: transient-5xx retry only for direct | ||
| // Google AI Studio; every other adapter keeps reset-only semantics here. | ||
| const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry; | ||
| return await fetchContinuationWithRetryPolicy( | ||
| recovery => { | ||
| noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind); | ||
| return fetchWithHeaderTimeout( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 2724
Pin all three verifiers to one immutable audit snapshot.
In
devlog/_plan/260818_bug_pr_resolution/010_wpv_stabilization_audit.md:7-8,32-36,46-47, captureBASE_SHA,HEAD_SHA, the CI run ID, and the lidge worktree from the same snapshot. Requiregit diffto use those fixed SHAs instead of movingorigin/mainandorigin/devrefs. Verify the CIheadShaandgit rev-parse HEADbefore recording results. IfHEAD_SHAchanges, invalidate all three results and restart the audit. Remove the “or successor” exception unless every verifier is rerun for that successor SHA.🤖 Prompt for AI Agents