diff --git a/devlog/_plan/261013_rt284_contrib_carry/000_plan.md b/devlog/_plan/261013_rt284_contrib_carry/000_plan.md new file mode 100644 index 00000000000..83a6e570a90 --- /dev/null +++ b/devlog/_plan/261013_rt284_contrib_carry/000_plan.md @@ -0,0 +1,77 @@ +# L2 — contributor carry for the 2.84.0 release train + +Lane L2, thread session `01a130c8-402a-7121-8f45-80144f1912d0`. The coordinator thread +`01a130be-2e10-7af0-adf9-efca9fe37e30` owns push, PR creation, merge, and release. + +- Worktree: `/private/tmp/rt284-lanes/L2` +- Branch: `codex/rt284-contrib-carry` +- Base: `origin/dev` `3e58c2ce3ee5433901ed467fe87c32fb4b5c5e77` +- Status file: `/private/tmp/rt284-lanes/status/L2.md` + +## Carry order and method + +Each source PR becomes one squashed commit made with `git merge --squash` of the PR head. The +commit keeps the original author (`git commit --author`), and its message carries the PR title, +the line `Carried from #N`, and a `Co-authored-by:` trailer for that author. + +| Order | PR | Author (trailer identity) | Head | Disposition | +|---|---|---|---|---| +| 1 | #7005 (fixes #7004) | Ingwannu | 5f81eab523 | CARRY | +| 2 | #7001 (#7000) | Ingwannu | 6f50b502e1 | CARRY | +| 3 | #7003 (#6764) | Robin Bially <7304732+robin-bially@users.noreply.github.com> | 08a4f130a9 | CARRY-WITH-FIX | +| 4 | #7002 (draft) | NineDayCC <110006840+NineDayCC@users.noreply.github.com> | 913e64ddcf | CARRY after independent review, else SKIP with reason | +| 5 | #6968 | LilMGenius | 78f75273d5 | CARRY | + +## #7003 fixes (separate commit after the carry, Co-authored-by Robin Bially) + +The fix commit answers Ingwannu's CHANGES_REQUESTED review of `08a4f130a9`: + +1. Scope `CHECKPOINT_CONTINUATION_NOTE` in `src/responses/compaction.ts` to work that was already + authorized and is still in scope. It must keep existing permission and confirmation gates, + state that a checkpoint or delegation grants no new authority, and let later user messages + that pause, cancel, or replace the task win. Prompt-contract fixtures cover a task that is + active but awaiting a decision, scoped delegated work, and a later pause/cancel/replacement. + These assert prompt text, not model behavior. +2. Add translated-route wire assertions. Each real adapter outbound request built from the + parsed context must keep both the pre-existing instructions and the note (CodeRabbit + `src/responses/parser.ts:594`). +3. Sync the structure docs that own `responses/parser.ts`, `responses/compaction.ts`, and the + passthrough path. They must record conditional application, summarizer/native-opaque + exclusions, the unchanged `SUMMARY_PREFIX`, and the authorization limit. + +The detailed design comes from the architect consultation recorded in `010_execution.md`. + +## Union constraints + +- `scripts/test-layout/layout.json` is 1999 lines on dev and must stay below 2000. The #7003 + entry is appended to an existing line, and later entries follow the same rule. +- New test files are registered in both `layout.json` `explicit` and + `tests/fixtures/test-layout-expected.json`. +- No lines are added to files at their `tests/fixtures/file-size-baseline.json` cap. New + cases go in sibling files. + +## Verification (C) + +Run with `OCX_TEST_RUNNER_BUN=/Users/jun/.cache/rt284-bun/bun-darwin-aarch64/bun` in +`/private/tmp/rt284-lanes/L2`: + +- Focused tests for each carried PR: + - `tests/codex-integration/codex-clamp-notice.test.ts` + - `tests/codex-integration/codex-quota-auto-refresh.test.ts` + - `tests/responses/compaction-continuation-note.test.ts` and the new adapter wire test + - `tests/codex-integration/main-account-credit-renewal.test.ts`, + `main-account-hard-lock-retirement.test.ts`, and `main-quota-provenance.test.ts` + - `gui/tests/lazycodex-role-models.test.tsx` +- `bun run typecheck` +- `tests/ci-workflows/file-size-ratchet.test.ts`, `tests/test-layout.test.ts`, and + `tests/test-layout-tooling.test.ts` +- `bun run structure:check` and `bun run privacy:scan` +- `bun run lint:gui` + +## Review and terminal state + +An independent sol reviewer checks the final head and returns PASS, NEAR-PASS, or FAIL. Any +NEAR-PASS residuals are folded or rebutted. The lane ends DONE with the final report, or +BLOCKED/NEEDS_HUMAN when a maintainer objection cannot be met in code or a guard cannot pass +without being weakened. + diff --git a/devlog/_plan/261013_rt284_contrib_carry/010_execution.md b/devlog/_plan/261013_rt284_contrib_carry/010_execution.md new file mode 100644 index 00000000000..42a8be8c2e1 --- /dev/null +++ b/devlog/_plan/261013_rt284_contrib_carry/010_execution.md @@ -0,0 +1,89 @@ +# 010 — L2 execution plan and architect consultation + +Architect: gpt-6.1-sol, handle `01a130c9-fbf3-7630-a0c0-84ac75a07b2f`. The proposal (D1–D6) was +read against PR #7003 head `08a4f130a9` and dev `3e58c2ce3e`. + +## Main dispositions + +| ID | Disposition | Plan | +|---|---|---| +| D1 | ACCEPT | Replace `CHECKPOINT_CONTINUATION_NOTE` in `src/responses/compaction.ts` with the architect's wording, which preserves authorization scope and permission gates, grants no authority through checkpoints or delegations, and gives later user messages priority. Application conditions, `SUMMARY_PREFIX`, and `appendCheckpointContinuationNote` stay unchanged. | +| D2 | ACCEPT | `tests/responses/compaction-continuation-note.test.ts` gains a literal `EXPECTED_NOTE` contract assertion and table-driven fixtures: authorized continuation, awaiting decision, scoped delegation, delegation awaiting authority, later pause, cancel, and replace. Each runs on the parsed context and the passthrough body. Test names say prompt contract or wire construction; none claims model behavior. | +| D3 | ACCEPT, bounded | NEW `tests/responses/compaction-continuation-wire.test.ts` builds real outbound requests from `parseRequest` output for OpenAI Chat, Anthropic, Google AI Studio, Kiro, Ollama native, and Command Code (Vertex and Cloud Code Assist only if their setup needs no ADC/network). It asserts that the route-specific system field holds the existing-instruction sentinel and the note. Controls: no checkpoint, opaque native blob, and summarizer turn lack the note. | +| D4 | AMEND | Per-adapter runTurn suites (Cursor, Devin, Claude CLI, CodeBuddy, Qoder) are not added in this carry. Instead, the wire test asserts that the shared `buildSystemPrompt` in `src/adapters/coding-agent/protocol.ts`, if it is exported (otherwise its nearest exported consumer), carries the note from a parsed context. The remaining runTurn routes are reported as uncovered. | +| D5 | ACCEPT | Append `compaction-continuation-note`, `-wire`, and (unused, so omitted) entries onto the existing `responses-main-quota-observation.test.ts` line in `scripts/test-layout/layout.json`. Register the same files in `tests/fixtures/test-layout-expected.json`. Line count stays at 1999. | +| D6 | ACCEPT | Add a "Decoded checkpoint continuation" section to `structure/transports/responses.md` and reflow existing compaction prose to stay within the 600-line limit without losing rules. Link it from `structure/transports/responses-wire-shapes.md`. Fix the authorization wording in `docs-site/src/content/docs/reference/adapters.md`. NEW `structure/decisions/ADR-7003-decoded-checkpoint-continuation.md`. `bun run structure:check` must pass. | + +## Commit sequence on `codex/rt284-contrib-carry` + +1. `fix(codex): deduplicate unchanged effort clamp observations (#7005)`: author Ingwannu, Co-authored-by Ingwannu. +2. `test(codex): drain quota fixture owners before home removal (#7001)`: author Ingwannu. +3. `fix(responses): frame a decoded ocx1 checkpoint as continuation in the system prompt (#7003)`: author Robin Bially. The layout.json entry is packed onto an existing line. +4. `fix(responses): scope the checkpoint continuation note to authorized work (#7003 review)`: D1–D6, Co-authored-by Robin Bially. +5. `fix(codex): retire stale short-window quota after plan changes (#7002)`: author NineDayCC, only after independent review, otherwise SKIP. +6. `fix(gui): cut the LazyCodex role section copy to what a user acts on (#6968)`: author LilMGenius. +7. `docs(devlog): L2 contributor carry plan`, containing these docs. + +## Acceptance + +The commands in `000_plan.md` §Verification all exit 0, `layout.json` stays below 2000 lines, and +the independent sol review returns PASS or NEAR-PASS with residuals folded on the final head. + + + +## A round 1 amendments (auditor `01a130d1-33a3`, verdict FAIL, four P2s) + +### B1 — exact note text (folded) + +`CHECKPOINT_CONTINUATION_NOTE` and the test's literal `EXPECTED_NOTE` are this exact string: + +> This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope. + +It keeps pending decisions and required permissions as gates ("wait for it before taking the +dependent action") and keeps delegation scope ("does not itself grant authority or expand the +task's scope"). It also gives later pause, cancellation, replacement, or scope change priority. +Nothing in it contradicts the existing "completed requests are history" framing. + +### B2 — structure budget (folded) + +`structure/transports/responses.md` and `structure/transports/responses-wire-shapes.md` are both +exactly 600 lines, and `sizeBudgetLines` is the limit. Every addition to either file is offset by +reflowing existing compaction prose in the same file, so both end at 600 lines or fewer and lose +no rule. The new section in `responses.md` carries exactly one owner link: +`> Decision record: [ADR-7003](../decisions/ADR-7003-decoded-checkpoint-continuation.md)`. +`responses-wire-shapes.md` gets a link sentence only. + +Review dispositions for every doc mapped to `src/responses/` or `src/adapters/` (structure/AGENTS.md:49): + +| Doc | Disposition | +|---|---| +| `transports/responses.md` | UPDATE (authority): new "Decoded checkpoint continuation" section | +| `transports/responses-wire-shapes.md` | UPDATE: link to the authority beside the compaction projection | +| `runtime.md`, `transports/byte-accounting.md`, `transports/inventory.md`, `data-planes/inbound-compat.md`, `providers-and-adapters.md`, `providers/cursor.md`, `providers/kiro.md`, `providers/xai-grok.md`, `providers/chat-compat.md`, `adapters/registry.md` | REVIEW: the builder greps each for compaction/checkpoint/system-prompt statements; a doc changes only when a local statement becomes inaccurate. Results go in the commit body. | + +### B3 — GUI union and full-suite disposition (folded) + +Verification gains these commands: + +- `cd gui && bun run build` (runs `tsc -b`, compiling all eleven locale catalogs) +- `OCX_TEST_RUNNER_BUN=… bun ../scripts/test-with-pinned-bun.ts --isolate tests/i18n-locales.test.ts tests/locale-parity.test.ts tests/lazycodex-role-models.test.tsx` from `gui/` + +The full `bun run test` is not run here. The documented AGENTS.md resource exception applies: +four release-train lanes share this host, and the coordinator runs exact-head hosted CI on the +stacked PRs plus a final `lane=all` Cross-platform run. The focused files above, together with +`bun run test:changed` against `3e58c2ce3e`, form the local evidence. Unrun coverage is reported +as left to CI. + +### B4 — commit shape and #7002 (rebutted, with one fold) + +The coordinator packet (codex_delegation input) states: "#7003: Ingwannu의 CHANGES_REQUESTED 리뷰 +내용을 읽고 요구사항을 별도 커밋으로 해결" (resolve the review in a separate commit). It also allows +"#7002(draft): 직접 리뷰해서 결함 없으면 carry, 아니면 수정 커밋 또는 SKIP 사유 보고". The separate +attributed fix commit (Co-authored-by Robin Bially) therefore follows the brief and stays. + +Fold: the independent #7002 review (`01a130d1-34d6`) returned CARRY with no blocking defect, so +#7002 is carried. Its two coverage gaps go into a follow-up commit co-authored by NineDayCC: an +omitted-tertiary variant beside the explicit-null delayed-credential cases, and a seeded +future-reset rotation tuple that a malformed or stale observation must leave intact +(`main-account-hard-lock-retirement.test.ts`). + diff --git a/devlog/_plan/261013_rt284_contrib_carry/020_final_review.md b/devlog/_plan/261013_rt284_contrib_carry/020_final_review.md new file mode 100644 index 00000000000..de2c4f35739 --- /dev/null +++ b/devlog/_plan/261013_rt284_contrib_carry/020_final_review.md @@ -0,0 +1,31 @@ +# 020 — L2 final review and report + +Work phase wp2. It covers the independent exact-head review and the lane's report to the +coordinator thread `01a130be-2e10-7af0-adf9-efca9fe37e30`. + +- Head: `codex/rt284-contrib-carry` `07a5cf2a7a18b6fa7aa62110b3b4c0af0e8c6a7a` (base `3e58c2ce3e`) +- Review: gpt-6.1-sol `01a130e7-46fa`, **PASS**, no actionable findings. It verified that every carry + matches its source PR's net delta, authorship and trailers, that Ingwannu's three #7003 requests + are resolved, that the #7002 coverage is meaningful, and that the union guards hold + (layout.json 1999 lines, structure docs 600 lines). +- Report: `/private/tmp/rt284-lanes/status/L2.md` and the lane's final message. +- Check: a fresh `cxc receipt test` of `/private/tmp/rt284-lanes/L2-check.sh` at the same head. + +| PR | Commit(s) | Verdict | +|---|---|---| +| #7005 | 419860b72e | CARRY | +| #7001 | 5f3481b2a7 | CARRY | +| #7003 | 4abec7a01d + 07a5cf2a7a | CARRY-WITH-FIX | +| #7002 | b1832a3e37 + 6a69e54666 | CARRY (with coverage) | +| #6968 | 88433c3dd0 | CARRY | + +Residual: model behavior for #6764, Cursor/Devin runTurn wire paths, and Windows/Linux hosted CI. + + +## Notes on the recorded order + +The two follow-up commits (6a69e54666 for #7002 and 07a5cf2a7a for #7003) land after the #6968 +carry rather than right after their source carries. The tree is the same either way. This +devlog commit sits on top of the reviewed code head and changes only `devlog/`, so the +coordinator may drop it without affecting any reviewed file. + diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index aa24e6c048f..f10ab318276 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -35,6 +35,11 @@ caller credential as the Pool selection. An exact account binding remains bound ## Config injection +`ocx sync` removes reasoning efforts unsupported by the selected Codex runtime from the catalog. +It reports a clamp when its runtime identity, removed efforts or affected models change; unchanged +syncs preserve the diagnostic timestamp and do not repeat that notice. This compatibility +projection does not require running `ocx doctor`. If a clamp clears and later returns, it is reported again. + OpenCodex serializes its Codex configuration writes. On Windows, a file temporarily held open by another process gets a short, bounded retry. If access stays busy, retry the operation shortly; OpenCodex preserves the existing lock evidence and does not use that access failure to take over @@ -1056,6 +1061,15 @@ For an opted-in, unpaused main account with a full usage window, the background Background revalidation is separate and off by default. It requires Token Guardian, the `openai` provider's `proactive` refresh policy, and `tokenGuardian.codexWarmupEnabled`. It skips accounts awaiting deferred registration validation. +### After a ChatGPT plan upgrade + +Refresh the main account's quota in Codex Auth after changing your ChatGPT plan. When the +authenticated usage response confirms a long window has replaced the five-hour window, +opencodex removes the old five-hour reading from both request admission and account selection. +An allowed response with primary and secondary windows can omit the optional tertiary window. +This recovery uses your included plan allowance and does not require enabling **Use credits**. +Partial or invalid usage data retains the previous block until recovery is confirmed. + ### Cancelling main-account device reauthentication When cancelling main-account device reauthentication, a temporary DELETE or network failure, or a response with an unknown or nonterminal status, keeps the active flow and the cancellation-failure indication so cancellation can be retried. Status polling normally continues so a login that completes can still be detected. If a retryable cancellation failure overlaps a non-2xx GET status response while the flow is `pending` or `committing`, either response order preserves cancellation retry on that same flow, with its last server-provided device code, verification URL, and phase. The GET HTTP failure still stops polling, but cancellation can be retried without starting a second login POST. A terminal `failed` response releases the flow and displays the normalized failure reason; only `succeeded` reports login success. A confirmed `cancelled` response releases the flow so a new device login can be started. A definitive HTTP 404 response with code `unknown_flow` also releases the expired flow ID so a new device login can be started, but does not report a successful login or confirmed cancellation. Late POST, GET, or DELETE responses from an earlier flow cannot change the new flow or report login success for it. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 839663c92b1..4e050d5d247 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -36,6 +36,10 @@ remain invalid. Ordinary function/custom tool results still require a nonempty `call_id`. The envelope metadata identifies a compatibility shape and grants no additional permissions. Native passthrough and routed (portable) compaction apply the same user-turn mapping to their raw bodies; the direct native `/responses/compact` forward keeps its existing raw-body rules. +## Checkpoint continuation after routed compaction + +A routed model replays an OpenCodeX `ocx1:` checkpoint as plain user text next to the retained earlier requests. When a request carries such a decoded checkpoint, OpenCodeX adds a short note to the system instructions (translated routes) or `instructions` (Responses passthrough). The note frames completed requests as history and permits continuation only of unfinished work already authorized and still within that scope, without asking for the same authorization again while it applies. Existing permission and confirmation gates remain in force: pending decisions or required permissions must be resolved before dependent actions. Checkpoints and delegations, including those from another thread, grant no authority or expanded scope. Later user messages take priority, including a pause, cancellation, replacement, or scope change. Compaction requests, opaque native blobs and requests without a checkpoint do not receive the note. + ## `openai-chat` **Targets:** OpenAI **Chat Completions** (`POST {baseUrl}/chat/completions`; a trailing `/chat/completions` or `/` on `baseUrl` is stripped first) and every compatible diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index a52321642b6..aadba5373ce 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1761,10 +1761,10 @@ export const de: Record = { "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der ~/.aside/u//models.json dieses Profils. Andere Provider bleiben unverändert. Beende Aside nach dem Anwenden vollständig und öffne es erneut.", "integrations.semantics.raycast": "Fügt einen OpenCodex-Provider-Eintrag in die providers.yaml von Raycast ein, damit jedes geroutete Modell in der Modellauswahl von Raycast AI erscheint. Raycast Pro erforderlich.", "integrations.semantics.omo": "Verwaltet ausschließlich providers.opencodex in der models.json von omo — ~/.omo/agent, sofern nicht OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR oder PI_CODING_AGENT_DIR sie umleitet. Ihre übrigen Provider bleiben unverändert. Gilt ab neuen Sitzungen.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Modelle der Codex-Agentenrollen", - "integrations.lazycodexRoles.hint": "Wählen Sie Modell und Reasoning-Aufwand, mit denen Codex jede Agentenrolle ausführt. Beim Speichern ändern sich nur die model- und effort-Zeilen in der Datei dieser Rolle unter $CODEX_HOME/agents. In omo.jsonc wird die Auswahl für LazyCodex nur übernommen, wenn sich die Datei sicher neu schreiben lässt.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex): Rollenmodelle", + "integrations.lazycodexRoles.hint": "Wählen Sie Modell und Reasoning-Aufwand jeder Rolle. Speichern aktualisiert ihre Datei unter $CODEX_HOME/agents und, wenn sie sich sicher neu schreiben lässt, die omo.jsonc von LazyCodex; mit „Aufwand erben“ entscheidet Codex über den Aufwand.", "integrations.lazycodexRoles.role": "Rolle", - "integrations.lazycodexRoles.current": "Aktuelles Modell", + "integrations.lazycodexRoles.current": "Aktuell", "integrations.lazycodexRoles.model": "Neues Modell", "integrations.lazycodexRoles.modelFor": "Modell für {role}", "integrations.lazycodexRoles.effort": "Reasoning-Aufwand", @@ -1777,14 +1777,14 @@ export const de: Record = { "integrations.lazycodexRoles.saveFailed": "Das Modell für {role} konnte nicht gespeichert werden.", "integrations.lazycodexRoles.saved": "{role} läuft jetzt mit {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} läuft jetzt mit {model}. omo.jsonc wurde nicht gefunden, daher wurde nur die Rollendatei geändert.", - "integrations.lazycodexRoles.omoComments": "{role} läuft laut Rollendatei jetzt mit {model}. omo.jsonc blieb unverändert, weil das Speichern seine Kommentare entfernen würde; setzen Sie [codex].agents.{role} dort von Hand.", - "integrations.lazycodexRoles.omoInvalid": "{role} läuft laut Rollendatei jetzt mit {model}. omo.jsonc blieb unverändert, weil es kein JSON-Objekt mit einem [codex].agents-Objekt ist.", - "integrations.lazycodexRoles.omoFailed": "{role} läuft laut Rollendatei jetzt mit {model}, aber omo.jsonc konnte nicht geschrieben werden.", + "integrations.lazycodexRoles.omoComments": "{role} läuft jetzt mit {model}. omo.jsonc enthält Kommentare, die beim Speichern verloren gehen würden, und blieb daher unverändert; setzen Sie [codex].agents.{role} dort selbst.", + "integrations.lazycodexRoles.omoInvalid": "{role} läuft jetzt mit {model}. omo.jsonc blieb unverändert, weil es kein gültiges JSON in der Form ist, die LazyCodex liest.", + "integrations.lazycodexRoles.omoFailed": "{role} läuft jetzt mit {model}, aber omo.jsonc konnte nicht geschrieben werden.", "integrations.lazycodexRoles.retryMirror": "omo.jsonc erneut versuchen", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc enthält Kommentare, daher ändert Speichern hier nur die Rollendateien. Passen Sie [codex].agents in omo.jsonc von Hand an, damit beide übereinstimmen.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc enthält Kommentare, daher ändert Speichern hier nur die Rollendateien. Setze [codex].agents in omo.jsonc selbst.", "integrations.lazycodexRoles.auto.button": "Automatisch zuweisen", "integrations.lazycodexRoles.auto.running": "Rollen werden eingestuft…", - "integrations.lazycodexRoles.auto.hint": "Jede Rolle mit deinem Standardmodell einstufen und für jede das günstigste ausreichende Modell vorschlagen. Nichts ändert sich, bevor du übernimmst.", + "integrations.lazycodexRoles.auto.hint": "Schlägt für jede Rolle das günstigste ausreichende Modell und den passenden Aufwand vor. Bis zum Übernehmen ändert sich nichts.", "integrations.lazycodexRoles.auto.failed": "Die Codex-Agentenrollen konnten nicht eingestuft werden.", "integrations.lazycodexRoles.auto.title": "Vorgeschlagene Modelle", "integrations.lazycodexRoles.auto.sizedWith": "Eingestuft mit {model}", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 8b32907a741..d08ea3f72a1 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -2372,10 +2372,10 @@ export const en = { "integrations.semantics.aside": "Manages only providers.opencodex in this profile’s ~/.aside/u//models.json. Your other providers stay unchanged. Fully quit and reopen Aside after applying.", "integrations.semantics.raycast": "Adds an OpenCodex provider entry to Raycast's providers.yaml so every routed model appears in the Raycast AI model picker. Raycast Pro required.", "integrations.semantics.omo": "Manages only providers.opencodex in omo's models.json — ~/.omo/agent unless OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR or PI_CODING_AGENT_DIR redirects it. Your other providers stay unchanged. Applies to new sessions.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex agent role models", - "integrations.lazycodexRoles.hint": "Pick the model and reasoning effort Codex runs for each agent role. Saving changes only the model and effort lines in that role's file under $CODEX_HOME/agents, and mirrors the choice into omo.jsonc for LazyCodex only when that file can be safely rewritten.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) role models", + "integrations.lazycodexRoles.hint": "Pick the model and effort each role runs on. Saving updates its file under $CODEX_HOME/agents, and LazyCodex's omo.jsonc when it can be safely rewritten; Inherit effort leaves the effort to Codex.", "integrations.lazycodexRoles.role": "Role", - "integrations.lazycodexRoles.current": "Current model", + "integrations.lazycodexRoles.current": "Current", "integrations.lazycodexRoles.model": "New model", "integrations.lazycodexRoles.modelFor": "Model for {role}", "integrations.lazycodexRoles.effort": "Reasoning effort", @@ -2388,14 +2388,14 @@ export const en = { "integrations.lazycodexRoles.saveFailed": "Could not save the model for {role}.", "integrations.lazycodexRoles.saved": "{role} now runs on {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} now runs on {model}. omo.jsonc was not found, so only the role file changed.", - "integrations.lazycodexRoles.omoComments": "{role} now runs on {model} in its role file. omo.jsonc was left unchanged because saving would remove its comments; set [codex].agents.{role} there by hand.", - "integrations.lazycodexRoles.omoInvalid": "{role} now runs on {model} in its role file. omo.jsonc was left unchanged because it is not a JSON object with a [codex].agents object.", - "integrations.lazycodexRoles.omoFailed": "{role} now runs on {model} in its role file, but omo.jsonc could not be written.", + "integrations.lazycodexRoles.omoComments": "{role} now runs on {model}. omo.jsonc has comments a save would remove, so it was left alone; set [codex].agents.{role} there yourself.", + "integrations.lazycodexRoles.omoInvalid": "{role} now runs on {model}. omo.jsonc was left alone because it is not valid JSON in the shape LazyCodex reads.", + "integrations.lazycodexRoles.omoFailed": "{role} now runs on {model}, but omo.jsonc could not be written.", "integrations.lazycodexRoles.retryMirror": "Retry omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc contains comments, so saving here changes only the role files. Edit [codex].agents in omo.jsonc by hand to keep it in step.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc has comments, so saving here updates only the role files. Set [codex].agents in omo.jsonc yourself.", "integrations.lazycodexRoles.auto.button": "Auto-assign", "integrations.lazycodexRoles.auto.running": "Sizing roles…", - "integrations.lazycodexRoles.auto.hint": "Size every role with your default model and propose the cheapest sufficient model for each. Nothing changes until you apply.", + "integrations.lazycodexRoles.auto.hint": "Suggest the cheapest model and effort that fit each role. Nothing changes until you apply.", "integrations.lazycodexRoles.auto.failed": "Could not size the Codex agent roles.", "integrations.lazycodexRoles.auto.title": "Proposed models", "integrations.lazycodexRoles.auto.sizedWith": "Sized with {model}", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 002f1cf2955..a4d9eb8c87d 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -2299,10 +2299,10 @@ export const fr: Record = { "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le fichier ~/.aside/u//models.json de ce profil. Vos autres fournisseurs restent inchangés. Quittez complètement Aside et relancez-le après application.", "integrations.semantics.raycast": "Ajoute une entrée de fournisseur OpenCodex dans le providers.yaml de Raycast afin que chaque modèle routé apparaisse dans le sélecteur de modèles de Raycast AI. Raycast Pro requis.", "integrations.semantics.omo": "Gère uniquement providers.opencodex dans le models.json d'omo — ~/.omo/agent, sauf redirection par OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR ou PI_CODING_AGENT_DIR. Vos autres fournisseurs restent inchangés. S'applique aux nouvelles sessions.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Modèles des rôles d’agent Codex", - "integrations.lazycodexRoles.hint": "Choisissez le modèle et l’effort de raisonnement que Codex utilise pour chaque rôle d’agent. L’enregistrement ne modifie que les lignes model et effort du fichier de ce rôle dans $CODEX_HOME/agents, et ne reporte le choix dans omo.jsonc pour LazyCodex que si ce fichier peut être réécrit sans risque.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) : modèles par rôle", + "integrations.lazycodexRoles.hint": "Choisissez le modèle et l'effort de raisonnement de chaque rôle. Enregistrer met à jour son fichier dans $CODEX_HOME/agents et, s'il peut être réécrit sans risque, le omo.jsonc de LazyCodex ; « Hériter de l’effort » laisse Codex choisir l'effort.", "integrations.lazycodexRoles.role": "Rôle", - "integrations.lazycodexRoles.current": "Modèle actuel", + "integrations.lazycodexRoles.current": "Actuel", "integrations.lazycodexRoles.model": "Nouveau modèle", "integrations.lazycodexRoles.modelFor": "Modèle pour {role}", "integrations.lazycodexRoles.effort": "Effort de raisonnement", @@ -2315,14 +2315,14 @@ export const fr: Record = { "integrations.lazycodexRoles.saveFailed": "Impossible d’enregistrer le modèle de {role}.", "integrations.lazycodexRoles.saved": "{role} utilise désormais {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} utilise désormais {model}. omo.jsonc est introuvable, seul le fichier du rôle a donc été modifié.", - "integrations.lazycodexRoles.omoComments": "{role} utilise désormais {model} dans son fichier de rôle. omo.jsonc n’a pas été modifié, car l’enregistrement supprimerait ses commentaires ; définissez [codex].agents.{role} à la main.", - "integrations.lazycodexRoles.omoInvalid": "{role} utilise désormais {model} dans son fichier de rôle. omo.jsonc n’a pas été modifié, car ce n’est pas un objet JSON contenant un objet [codex].agents.", - "integrations.lazycodexRoles.omoFailed": "{role} utilise désormais {model} dans son fichier de rôle, mais omo.jsonc n’a pas pu être écrit.", + "integrations.lazycodexRoles.omoComments": "{role} utilise désormais {model}. omo.jsonc contient des commentaires qu'un enregistrement supprimerait ; il n'a pas été modifié, définissez [codex].agents.{role} vous-même.", + "integrations.lazycodexRoles.omoInvalid": "{role} utilise désormais {model}. omo.jsonc n'a pas été modifié, car ce n'est pas un JSON valide de la forme que LazyCodex lit.", + "integrations.lazycodexRoles.omoFailed": "{role} utilise désormais {model}, mais omo.jsonc n'a pas pu être écrit.", "integrations.lazycodexRoles.retryMirror": "Réessayer omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc contient des commentaires : l’enregistrement ici ne modifie donc que les fichiers de rôle. Modifiez [codex].agents dans omo.jsonc à la main pour rester synchronisé.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc contient des commentaires : enregistrer ici ne met à jour que les fichiers de rôle. Définissez [codex].agents dans omo.jsonc vous-même.", "integrations.lazycodexRoles.auto.button": "Attribution auto", "integrations.lazycodexRoles.auto.running": "Évaluation des rôles…", - "integrations.lazycodexRoles.auto.hint": "Évaluer chaque rôle avec votre modèle par défaut et proposer pour chacun le modèle suffisant le moins cher. Rien ne change avant que vous appliquiez.", + "integrations.lazycodexRoles.auto.hint": "Propose pour chaque rôle le modèle suffisant le moins cher et l'effort adapté. Rien ne change avant que vous appliquiez.", "integrations.lazycodexRoles.auto.failed": "Impossible d'évaluer les rôles d'agent Codex.", "integrations.lazycodexRoles.auto.title": "Modèles proposés", "integrations.lazycodexRoles.auto.sizedWith": "Évalué avec {model}", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 6eb6908d816..242ce560b21 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2194,10 +2194,10 @@ export const ja: Record = { "integrations.semantics.aside": "このプロファイルの ~/.aside/u//models.json 内の providers.opencodex のみを管理します。他のプロバイダーは変更しません。適用後は Aside を完全に終了してから開き直してください。", "integrations.semantics.raycast": "Raycast の providers.yaml に OpenCodex のプロバイダーエントリを追加し、ルーティングされたすべてのモデルを Raycast AI のモデル選択に表示します。Raycast Pro が必要です。", "integrations.semantics.omo": "omo の models.json にある providers.opencodex のみを管理します。場所は ~/.omo/agent で、OMO_CODING_AGENT_DIR・SENPI_CODING_AGENT_DIR・PI_CODING_AGENT_DIR のいずれかが設定されている場合はそちらが優先されます。他のプロバイダーは変更しません。新しいセッションから適用されます。", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex エージェントロールのモデル", - "integrations.lazycodexRoles.hint": "Codex が各エージェントロールで使うモデルと推論負荷を選びます。保存すると $CODEX_HOME/agents にあるそのロールのファイルの model 行と effort 行だけが変わり、omo.jsonc を安全に書き換えられる場合に限り LazyCodex 用に同じ値を反映します。", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) のロールモデル", + "integrations.lazycodexRoles.hint": "各ロールが使うモデルと推論負荷を選びます。保存すると $CODEX_HOME/agents のロールファイルが更新され、安全に書き換えられる場合は LazyCodex の omo.jsonc も更新されます。「推論エフォートを継承」を選ぶと推論負荷は Codex に任せます。", "integrations.lazycodexRoles.role": "ロール", - "integrations.lazycodexRoles.current": "現在のモデル", + "integrations.lazycodexRoles.current": "現在", "integrations.lazycodexRoles.model": "新しいモデル", "integrations.lazycodexRoles.modelFor": "{role} のモデル", "integrations.lazycodexRoles.effort": "推論負荷", @@ -2210,14 +2210,14 @@ export const ja: Record = { "integrations.lazycodexRoles.saveFailed": "{role} のモデルを保存できませんでした。", "integrations.lazycodexRoles.saved": "{role} は {model} で動作します。", "integrations.lazycodexRoles.omoAbsent": "{role} は {model} で動作します。omo.jsonc が見つからないため、ロールファイルだけを変更しました。", - "integrations.lazycodexRoles.omoComments": "ロールファイルでは {role} が {model} で動作します。保存するとコメントが消えるため omo.jsonc は変更していません。[codex].agents.{role} を手動で設定してください。", - "integrations.lazycodexRoles.omoInvalid": "ロールファイルでは {role} が {model} で動作します。omo.jsonc は [codex].agents オブジェクトを持つ JSON オブジェクトではないため変更していません。", - "integrations.lazycodexRoles.omoFailed": "ロールファイルでは {role} が {model} で動作しますが、omo.jsonc に書き込めませんでした。", + "integrations.lazycodexRoles.omoComments": "{role} は {model} で動くようになりました。omo.jsonc には保存で消えるコメントがあるため変更していません。[codex].agents.{role} はご自身で設定してください。", + "integrations.lazycodexRoles.omoInvalid": "{role} は {model} で動くようになりました。omo.jsonc は LazyCodex が読む形の有効な JSON ではないため変更していません。", + "integrations.lazycodexRoles.omoFailed": "{role} は {model} で動くようになりましたが、omo.jsonc は書き込めませんでした。", "integrations.lazycodexRoles.retryMirror": "omo.jsonc を再試行", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc にコメントがあるため、ここで保存するとロールファイルだけが変わります。omo.jsonc の [codex].agents は手動で合わせてください。", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc にコメントがあるため、ここで保存するとロールファイルだけが更新されます。omo.jsonc の [codex].agents はご自身で設定してください。", "integrations.lazycodexRoles.auto.button": "自動割り当て", "integrations.lazycodexRoles.auto.running": "ロールを評価中…", - "integrations.lazycodexRoles.auto.hint": "既定のモデルで各ロールを評価し、それぞれに十分で最も安いモデルを提案します。適用するまで何も変わりません。", + "integrations.lazycodexRoles.auto.hint": "ロールごとに十分で最も安いモデルと推論負荷を提案します。適用するまで何も変わりません。", "integrations.lazycodexRoles.auto.failed": "Codex エージェントロールを評価できませんでした。", "integrations.lazycodexRoles.auto.title": "提案されたモデル", "integrations.lazycodexRoles.auto.sizedWith": "{model} で評価", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 5f2d2a96c89..a846a96ae00 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1805,10 +1805,10 @@ export const ko: Record = { "integrations.semantics.aside": "이 프로필의 ~/.aside/u//models.json에서 providers.opencodex만 관리합니다. 다른 프로바이더는 그대로 유지됩니다. 적용 후 Aside를 완전히 종료하고 다시 여세요.", "integrations.semantics.raycast": "Raycast의 providers.yaml에 OpenCodex 프로바이더 항목을 추가해 라우팅된 모든 모델이 Raycast AI 모델 선택기에 표시되도록 합니다. Raycast Pro가 필요합니다.", "integrations.semantics.omo": "omo의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.omo/agent이며 OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR, PI_CODING_AGENT_DIR 중 설정된 값이 있으면 그쪽이 우선합니다. 다른 프로바이더는 그대로 유지됩니다. 새 세션부터 적용됩니다.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex 에이전트 역할 모델", - "integrations.lazycodexRoles.hint": "Codex가 에이전트 역할마다 쓰는 모델과 추론 강도를 고릅니다. 저장하면 $CODEX_HOME/agents에 있는 그 역할 파일의 model 줄과 effort 줄만 바뀌고, omo.jsonc를 안전하게 다시 쓸 수 있을 때만 LazyCodex용으로 같은 값을 반영합니다.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) 역할 모델", + "integrations.lazycodexRoles.hint": "역할마다 쓸 모델과 추론 강도를 고릅니다. 저장하면 $CODEX_HOME/agents의 역할 파일이 바뀌고, LazyCodex의 omo.jsonc도 안전하게 다시 쓸 수 있으면 함께 바뀝니다. '추론 강도 상속'을 고르면 추론 강도는 Codex가 정합니다.", "integrations.lazycodexRoles.role": "역할", - "integrations.lazycodexRoles.current": "현재 모델", + "integrations.lazycodexRoles.current": "현재 설정", "integrations.lazycodexRoles.model": "새 모델", "integrations.lazycodexRoles.modelFor": "{role} 모델", "integrations.lazycodexRoles.effort": "추론 강도", @@ -1821,14 +1821,14 @@ export const ko: Record = { "integrations.lazycodexRoles.saveFailed": "{role}의 모델을 저장하지 못했습니다.", "integrations.lazycodexRoles.saved": "이제 {role}은(는) {model}로 실행됩니다.", "integrations.lazycodexRoles.omoAbsent": "이제 {role}은(는) {model}로 실행됩니다. omo.jsonc가 없어서 역할 파일만 바꿨습니다.", - "integrations.lazycodexRoles.omoComments": "역할 파일에서 {role}은(는) 이제 {model}로 실행됩니다. 저장하면 주석이 사라지므로 omo.jsonc는 바꾸지 않았습니다. 그곳의 [codex].agents.{role}은 직접 설정하세요.", - "integrations.lazycodexRoles.omoInvalid": "역할 파일에서 {role}은(는) 이제 {model}로 실행됩니다. omo.jsonc가 [codex].agents 객체를 가진 JSON 객체가 아니어서 바꾸지 않았습니다.", - "integrations.lazycodexRoles.omoFailed": "역할 파일에서 {role}은(는) 이제 {model}로 실행되지만, omo.jsonc에는 쓰지 못했습니다.", + "integrations.lazycodexRoles.omoComments": "{role}은(는) 이제 {model}로 실행됩니다. omo.jsonc에는 저장하면 사라질 주석이 있어 바꾸지 않았으니, 그곳의 [codex].agents.{role}은(는) 직접 맞춰 주세요.", + "integrations.lazycodexRoles.omoInvalid": "{role}은(는) 이제 {model}로 실행됩니다. omo.jsonc가 LazyCodex가 읽는 형태의 올바른 JSON이 아니어서 그 파일은 바꾸지 않았습니다.", + "integrations.lazycodexRoles.omoFailed": "{role}은(는) 이제 {model}로 실행되지만, omo.jsonc는 쓰지 못했습니다.", "integrations.lazycodexRoles.retryMirror": "omo.jsonc 다시 시도", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc에 주석이 있어서 여기서 저장하면 역할 파일만 바뀝니다. omo.jsonc의 [codex].agents는 직접 맞춰 주세요.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc에 주석이 있어 여기서 저장하면 역할 파일만 바뀝니다. omo.jsonc의 [codex].agents는 직접 맞춰 주세요.", "integrations.lazycodexRoles.auto.button": "자동 배정", "integrations.lazycodexRoles.auto.running": "역할 평가 중…", - "integrations.lazycodexRoles.auto.hint": "기본 모델로 모든 역할을 평가하고 역할마다 충분하면서 가장 저렴한 모델을 제안합니다. 적용하기 전에는 아무것도 바뀌지 않습니다.", + "integrations.lazycodexRoles.auto.hint": "역할마다 충분하면서 가장 저렴한 모델과 추론 강도를 제안합니다. 적용하기 전에는 아무것도 바뀌지 않습니다.", "integrations.lazycodexRoles.auto.failed": "Codex 에이전트 역할을 평가하지 못했습니다.", "integrations.lazycodexRoles.auto.title": "제안된 모델", "integrations.lazycodexRoles.auto.sizedWith": "{model}(으)로 평가함", diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts index 299456162f3..29d76fd860a 100644 --- a/gui/src/i18n/pt.ts +++ b/gui/src/i18n/pt.ts @@ -2345,10 +2345,10 @@ export const pt: Record = { "integrations.semantics.aside": "Gerencia apenas providers.opencodex no ~/.aside/u//models.json deste perfil. Seus outros provedores permanecem inalterados. Feche completamente e reabra o Aside após aplicar.", "integrations.semantics.raycast": "Adiciona uma entrada de provedor OpenCodex ao providers.yaml do Raycast, para que todos os modelos roteados apareçam no seletor de modelos do Raycast AI. Requer Raycast Pro.", "integrations.semantics.omo": "Gerencia apenas providers.opencodex no models.json do omo — ~/.omo/agent, a menos que OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR ou PI_CODING_AGENT_DIR o redirecione. Seus outros provedores permanecem inalterados. Vale para novas sessões.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Modelos das funções de agente do Codex", - "integrations.lazycodexRoles.hint": "Escolha o modelo e o esforço de raciocínio que o Codex usa em cada função de agente. Ao salvar, só as linhas de modelo e esforço no arquivo da função em $CODEX_HOME/agents são alteradas, e a escolha é espelhada em omo.jsonc para o LazyCodex apenas quando esse arquivo puder ser reescrito com segurança.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex): modelos por papel", + "integrations.lazycodexRoles.hint": "Escolha o modelo e o esforço de raciocínio de cada papel. Salvar atualiza o arquivo dele em $CODEX_HOME/agents e, quando puder ser reescrito com segurança, o omo.jsonc do LazyCodex; \"Herdar esforço\" deixa o esforço para o Codex.", "integrations.lazycodexRoles.role": "Função", - "integrations.lazycodexRoles.current": "Modelo atual", + "integrations.lazycodexRoles.current": "Atual", "integrations.lazycodexRoles.model": "Novo modelo", "integrations.lazycodexRoles.modelFor": "Modelo para {role}", "integrations.lazycodexRoles.effort": "Esforço de raciocínio", @@ -2361,14 +2361,14 @@ export const pt: Record = { "integrations.lazycodexRoles.saveFailed": "Não foi possível salvar o modelo de {role}.", "integrations.lazycodexRoles.saved": "{role} agora usa {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} agora usa {model}. O omo.jsonc não foi encontrado, então só o arquivo da função foi alterado.", - "integrations.lazycodexRoles.omoComments": "{role} agora usa {model} no arquivo da função. O omo.jsonc não foi alterado porque salvar removeria seus comentários; defina [codex].agents.{role} nele manualmente.", - "integrations.lazycodexRoles.omoInvalid": "{role} agora usa {model} no arquivo da função. O omo.jsonc não foi alterado porque não é um objeto JSON com um objeto [codex].agents.", - "integrations.lazycodexRoles.omoFailed": "{role} agora usa {model} no arquivo da função, mas não foi possível gravar o omo.jsonc.", + "integrations.lazycodexRoles.omoComments": "{role} agora usa {model}. O omo.jsonc tem comentários que salvar removeria, então ficou como estava; defina [codex].agents.{role} nele você mesmo.", + "integrations.lazycodexRoles.omoInvalid": "{role} agora usa {model}. O omo.jsonc ficou como estava porque não é um JSON válido no formato que o LazyCodex lê.", + "integrations.lazycodexRoles.omoFailed": "{role} agora usa {model}, mas não foi possível gravar o omo.jsonc.", "integrations.lazycodexRoles.retryMirror": "Tentar omo.jsonc novamente", - "integrations.lazycodexRoles.omoCommentsState": "O omo.jsonc contém comentários, então salvar aqui altera apenas os arquivos das funções. Edite [codex].agents no omo.jsonc manualmente para mantê-lo sincronizado.", + "integrations.lazycodexRoles.omoCommentsState": "O omo.jsonc tem comentários, então salvar aqui atualiza só os arquivos de papel. Defina [codex].agents no omo.jsonc você mesmo.", "integrations.lazycodexRoles.auto.button": "Atribuir automaticamente", "integrations.lazycodexRoles.auto.running": "Dimensionando funções…", - "integrations.lazycodexRoles.auto.hint": "Dimensiona cada função com seu modelo padrão e propõe o modelo suficiente mais barato para cada uma. Nada muda até você aplicar.", + "integrations.lazycodexRoles.auto.hint": "Sugere para cada papel o modelo suficiente mais barato e o esforço adequado. Nada muda até você aplicar.", "integrations.lazycodexRoles.auto.failed": "Não foi possível dimensionar as funções de agente do Codex.", "integrations.lazycodexRoles.auto.title": "Modelos propostos", "integrations.lazycodexRoles.auto.sizedWith": "Dimensionado com {model}", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index e11cd3ce4b9..92891a8c5df 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2285,10 +2285,10 @@ export const ru: Record = { "integrations.semantics.aside": "Управляет только providers.opencodex в файле ~/.aside/u//models.json этого профиля. Другие провайдеры остаются без изменений. После применения полностью закройте и снова откройте Aside.", "integrations.semantics.raycast": "Добавляет запись провайдера OpenCodex в providers.yaml Raycast, чтобы каждая маршрутизируемая модель появилась в выборе моделей Raycast AI. Требуется Raycast Pro.", "integrations.semantics.omo": "Управляет только providers.opencodex в models.json omo — ~/.omo/agent, если только OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR или PI_CODING_AGENT_DIR не перенаправляет путь. Остальные провайдеры остаются без изменений. Применяется к новым сессиям.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Модели ролей агентов Codex", - "integrations.lazycodexRoles.hint": "Выберите модель и уровень рассуждений, с которыми Codex запускает каждую роль агента. При сохранении меняются только строки model и effort в файле этой роли в $CODEX_HOME/agents, а в omo.jsonc для LazyCodex выбор копируется, только если этот файл можно безопасно перезаписать.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex): модели ролей", + "integrations.lazycodexRoles.hint": "Выберите модель и уровень рассуждений для каждой роли. Сохранение обновляет её файл в $CODEX_HOME/agents и, если его можно безопасно перезаписать, omo.jsonc LazyCodex; при «Наследовать усилие» уровень выбирает Codex.", "integrations.lazycodexRoles.role": "Роль", - "integrations.lazycodexRoles.current": "Текущая модель", + "integrations.lazycodexRoles.current": "Сейчас", "integrations.lazycodexRoles.model": "Новая модель", "integrations.lazycodexRoles.modelFor": "Модель для {role}", "integrations.lazycodexRoles.effort": "Уровень рассуждений", @@ -2301,14 +2301,14 @@ export const ru: Record = { "integrations.lazycodexRoles.saveFailed": "Не удалось сохранить модель для {role}.", "integrations.lazycodexRoles.saved": "{role} теперь работает на {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} теперь работает на {model}. Файл omo.jsonc не найден, поэтому изменён только файл роли.", - "integrations.lazycodexRoles.omoComments": "В файле роли {role} теперь работает на {model}. omo.jsonc не изменён, потому что сохранение удалило бы его комментарии; задайте [codex].agents.{role} вручную.", - "integrations.lazycodexRoles.omoInvalid": "В файле роли {role} теперь работает на {model}. omo.jsonc не изменён: это не JSON-объект с объектом [codex].agents.", - "integrations.lazycodexRoles.omoFailed": "В файле роли {role} теперь работает на {model}, но записать omo.jsonc не удалось.", + "integrations.lazycodexRoles.omoComments": "{role} теперь работает на {model}. В omo.jsonc есть комментарии, которые сохранение удалило бы, поэтому файл не изменён; задайте [codex].agents.{role} в нём сами.", + "integrations.lazycodexRoles.omoInvalid": "{role} теперь работает на {model}. omo.jsonc не изменён: это не корректный JSON той структуры, которую читает LazyCodex.", + "integrations.lazycodexRoles.omoFailed": "{role} теперь работает на {model}, но записать omo.jsonc не удалось.", "integrations.lazycodexRoles.retryMirror": "Повторить запись omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "В omo.jsonc есть комментарии, поэтому сохранение здесь меняет только файлы ролей. Поправьте [codex].agents в omo.jsonc вручную, чтобы они совпадали.", + "integrations.lazycodexRoles.omoCommentsState": "В omo.jsonc есть комментарии, поэтому сохранение здесь обновляет только файлы ролей. Задайте [codex].agents в omo.jsonc сами.", "integrations.lazycodexRoles.auto.button": "Автоназначение", "integrations.lazycodexRoles.auto.running": "Оценка ролей…", - "integrations.lazycodexRoles.auto.hint": "Оценить каждую роль моделью по умолчанию и предложить для каждой самую дешёвую достаточную модель. До применения ничего не меняется.", + "integrations.lazycodexRoles.auto.hint": "Предлагает для каждой роли самую дешёвую подходящую модель и уровень рассуждений. До применения ничего не меняется.", "integrations.lazycodexRoles.auto.failed": "Не удалось оценить роли агентов Codex.", "integrations.lazycodexRoles.auto.title": "Предложенные модели", "integrations.lazycodexRoles.auto.sizedWith": "Оценено моделью {model}", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 57e283fe903..e9a667b53f9 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2311,10 +2311,10 @@ export const tr: Record = { "integrations.semantics.aside": "Yalnızca bu profilin ~/.aside/u//models.json dosyasındaki providers.opencodex bölümünü yönetir. Diğer sağlayıcılarınız değişmez. Uyguladıktan sonra Aside’ı tamamen kapatıp yeniden açın.", "integrations.semantics.raycast": "Raycast'in providers.yaml dosyasına bir OpenCodex sağlayıcı girdisi ekler; böylece yönlendirilen her model Raycast AI model seçicisinde görünür. Raycast Pro gerekir.", "integrations.semantics.omo": "Yalnızca omo'nun models.json dosyasındaki providers.opencodex girdisini yönetir — OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR veya PI_CODING_AGENT_DIR yönlendirmediği sürece ~/.omo/agent. Diğer sağlayıcılarınız değişmeden kalır. Yeni oturumlardan itibaren geçerlidir.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex ajan rolü modelleri", - "integrations.lazycodexRoles.hint": "Codex'in her ajan rolünde kullanacağı modeli ve akıl yürütme çabasını seçin. Kaydetmek yalnızca $CODEX_HOME/agents altındaki o rol dosyasının model ve effort satırlarını değiştirir ve omo.jsonc güvenle yeniden yazılabiliyorsa seçimi LazyCodex için oraya da yansıtır.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) rol modelleri", + "integrations.lazycodexRoles.hint": "Her rolün modelini ve akıl yürütme çabasını seçin. Kaydetmek rolün $CODEX_HOME/agents altındaki dosyasını ve güvenle yeniden yazılabiliyorsa LazyCodex'in omo.jsonc dosyasını günceller; \"Çabayı devral\" çabayı Codex'e bırakır.", "integrations.lazycodexRoles.role": "Rol", - "integrations.lazycodexRoles.current": "Geçerli model", + "integrations.lazycodexRoles.current": "Şu an", "integrations.lazycodexRoles.model": "Yeni model", "integrations.lazycodexRoles.modelFor": "{role} için model", "integrations.lazycodexRoles.effort": "Akıl yürütme çabası", @@ -2327,14 +2327,14 @@ export const tr: Record = { "integrations.lazycodexRoles.saveFailed": "{role} için model kaydedilemedi.", "integrations.lazycodexRoles.saved": "{role} artık {model} ile çalışıyor.", "integrations.lazycodexRoles.omoAbsent": "{role} artık {model} ile çalışıyor. omo.jsonc bulunamadığı için yalnızca rol dosyası değişti.", - "integrations.lazycodexRoles.omoComments": "{role} rol dosyasında artık {model} ile çalışıyor. Kaydetmek yorumlarını sileceği için omo.jsonc değiştirilmedi; [codex].agents.{role} değerini orada elle ayarlayın.", - "integrations.lazycodexRoles.omoInvalid": "{role} rol dosyasında artık {model} ile çalışıyor. omo.jsonc, [codex].agents nesnesi içeren bir JSON nesnesi olmadığı için değiştirilmedi.", - "integrations.lazycodexRoles.omoFailed": "{role} rol dosyasında artık {model} ile çalışıyor, ancak omo.jsonc yazılamadı.", + "integrations.lazycodexRoles.omoComments": "{role} artık {model} ile çalışıyor. omo.jsonc kaydetmenin sileceği yorumlar içerdiği için değiştirilmedi; oradaki [codex].agents.{role} değerini kendiniz ayarlayın.", + "integrations.lazycodexRoles.omoInvalid": "{role} artık {model} ile çalışıyor. omo.jsonc, LazyCodex'in okuduğu biçimde geçerli bir JSON olmadığı için değiştirilmedi.", + "integrations.lazycodexRoles.omoFailed": "{role} artık {model} ile çalışıyor, ancak omo.jsonc yazılamadı.", "integrations.lazycodexRoles.retryMirror": "omo.jsonc'yi yeniden dene", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc yorum içerdiği için burada kaydetmek yalnızca rol dosyalarını değiştirir. Eşleşmeleri için omo.jsonc içindeki [codex].agents değerini elle düzenleyin.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc yorumlar içeriyor, bu yüzden burada kaydetmek yalnızca rol dosyalarını günceller. omo.jsonc içindeki [codex].agents değerini kendiniz ayarlayın.", "integrations.lazycodexRoles.auto.button": "Otomatik ata", "integrations.lazycodexRoles.auto.running": "Roller değerlendiriliyor…", - "integrations.lazycodexRoles.auto.hint": "Her rolü varsayılan modelinle değerlendir ve her biri için yeterli en ucuz modeli öner. Uygulayana kadar hiçbir şey değişmez.", + "integrations.lazycodexRoles.auto.hint": "Her rol için yeterli olan en ucuz modeli ve çabayı önerir. Uygulayana kadar hiçbir şey değişmez.", "integrations.lazycodexRoles.auto.failed": "Codex ajan rolleri değerlendirilemedi.", "integrations.lazycodexRoles.auto.title": "Önerilen modeller", "integrations.lazycodexRoles.auto.sizedWith": "{model} ile değerlendirildi", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 96858b1c670..2c51b8a5206 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -2320,10 +2320,10 @@ export const vi: Record = { "integrations.semantics.aside": "Chỉ quản lý providers.opencodex trong ~/.aside/u//models.json của profile này. Các provider khác của bạn không thay đổi. Hãy thoát hoàn toàn và mở lại Aside sau khi áp dụng.", "integrations.semantics.raycast": "Thêm một provider OpenCodex vào providers.yaml của Raycast để mọi model được định tuyến xuất hiện trong bộ chọn model AI của Raycast. Yêu cầu Raycast Pro.", "integrations.semantics.omo": "Chỉ quản lý providers.opencodex trong models.json của omo — ~/.omo/agent, trừ khi OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR hoặc PI_CODING_AGENT_DIR chuyển hướng. Các nhà cung cấp khác của bạn không thay đổi. Áp dụng cho phiên mới.", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Mô hình cho vai trò tác tử Codex", - "integrations.lazycodexRoles.hint": "Chọn mô hình và mức suy luận Codex dùng cho từng vai trò tác tử. Khi lưu, chỉ các dòng model và effort trong tệp của vai trò đó ở $CODEX_HOME/agents thay đổi; lựa chọn chỉ được ghi sang omo.jsonc cho LazyCodex khi tệp đó có thể được ghi lại an toàn.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex): model theo vai trò", + "integrations.lazycodexRoles.hint": "Chọn model và mức suy luận cho từng vai trò. Lưu sẽ cập nhật tệp của vai trò trong $CODEX_HOME/agents, và cả omo.jsonc của LazyCodex khi tệp đó có thể được ghi lại an toàn; \"Kế thừa mức suy luận\" để Codex quyết định mức này.", "integrations.lazycodexRoles.role": "Vai trò", - "integrations.lazycodexRoles.current": "Mô hình hiện tại", + "integrations.lazycodexRoles.current": "Hiện tại", "integrations.lazycodexRoles.model": "Mô hình mới", "integrations.lazycodexRoles.modelFor": "Mô hình cho {role}", "integrations.lazycodexRoles.effort": "Effort lý luận", @@ -2336,14 +2336,14 @@ export const vi: Record = { "integrations.lazycodexRoles.saveFailed": "Không lưu được mô hình cho {role}.", "integrations.lazycodexRoles.saved": "{role} giờ chạy trên {model}.", "integrations.lazycodexRoles.omoAbsent": "{role} giờ chạy trên {model}. Không tìm thấy omo.jsonc nên chỉ tệp vai trò được thay đổi.", - "integrations.lazycodexRoles.omoComments": "Trong tệp vai trò, {role} giờ chạy trên {model}. omo.jsonc được giữ nguyên vì lưu sẽ xóa các chú thích của nó; hãy tự đặt [codex].agents.{role} ở đó.", - "integrations.lazycodexRoles.omoInvalid": "Trong tệp vai trò, {role} giờ chạy trên {model}. omo.jsonc được giữ nguyên vì nó không phải đối tượng JSON có đối tượng [codex].agents.", - "integrations.lazycodexRoles.omoFailed": "Trong tệp vai trò, {role} giờ chạy trên {model}, nhưng không ghi được omo.jsonc.", + "integrations.lazycodexRoles.omoComments": "{role} giờ chạy bằng {model}. omo.jsonc có chú thích sẽ bị xóa khi lưu nên được giữ nguyên; hãy tự đặt [codex].agents.{role} trong đó.", + "integrations.lazycodexRoles.omoInvalid": "{role} giờ chạy bằng {model}. omo.jsonc được giữ nguyên vì nó không phải JSON hợp lệ theo dạng LazyCodex đọc.", + "integrations.lazycodexRoles.omoFailed": "{role} giờ chạy bằng {model}, nhưng không ghi được omo.jsonc.", "integrations.lazycodexRoles.retryMirror": "Thử lại omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc có chú thích nên lưu ở đây chỉ thay đổi các tệp vai trò. Hãy tự sửa [codex].agents trong omo.jsonc để hai nơi khớp nhau.", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc có chú thích, nên lưu ở đây chỉ cập nhật các tệp vai trò. Hãy tự đặt [codex].agents trong omo.jsonc.", "integrations.lazycodexRoles.auto.button": "Tự động gán", "integrations.lazycodexRoles.auto.running": "Đang đánh giá vai trò…", - "integrations.lazycodexRoles.auto.hint": "Đánh giá mọi vai trò bằng mô hình mặc định và đề xuất mô hình đủ dùng rẻ nhất cho từng vai trò. Không có gì thay đổi cho đến khi bạn áp dụng.", + "integrations.lazycodexRoles.auto.hint": "Đề xuất cho mỗi vai trò model rẻ nhất mà vẫn đủ dùng cùng effort lý luận phù hợp. Không có gì thay đổi cho tới khi bạn áp dụng.", "integrations.lazycodexRoles.auto.failed": "Không thể đánh giá các vai trò tác tử Codex.", "integrations.lazycodexRoles.auto.title": "Mô hình đề xuất", "integrations.lazycodexRoles.auto.sizedWith": "Đánh giá bằng {model}", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 8cdd0a2e983..12c3442b30c 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -3211,10 +3211,10 @@ export const zhTW: Record = { "integrations.semantics.aside": "僅管理此設定檔的 ~/.aside/u//models.json 中的 providers.opencodex。其他供應商維持不變。套用後請完全結束並重新開啟 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中新增一個 OpenCodex 供應商項目,讓所有已路由的模型出現在 Raycast AI 模型選擇器中。需要 Raycast Pro。", "integrations.semantics.omo": "僅管理 omo 的 models.json 中的 providers.opencodex,路徑為 ~/.omo/agent,若設定了 OMO_CODING_AGENT_DIR、SENPI_CODING_AGENT_DIR 或 PI_CODING_AGENT_DIR 則以其為準。你的其他供應商維持不變。對新工作階段生效。", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex 代理角色模型", - "integrations.lazycodexRoles.hint": "為每個代理角色選擇 Codex 使用的模型和推理強度。儲存時只會修改 $CODEX_HOME/agents 中該角色檔案的 model 行和 effort 行;只有在 omo.jsonc 可以安全改寫時,才會把選擇同步到供 LazyCodex 讀取的 omo.jsonc。", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) 角色模型", + "integrations.lazycodexRoles.hint": "為每個角色選擇模型與推理強度。儲存會更新 $CODEX_HOME/agents 中的角色檔案,並在可以安全改寫時一併更新 LazyCodex 的 omo.jsonc;選「繼承推理強度」則由 Codex 決定推理強度。", "integrations.lazycodexRoles.role": "角色", - "integrations.lazycodexRoles.current": "目前模型", + "integrations.lazycodexRoles.current": "目前", "integrations.lazycodexRoles.model": "新模型", "integrations.lazycodexRoles.modelFor": "{role} 的模型", "integrations.lazycodexRoles.effort": "推理強度", @@ -3227,14 +3227,14 @@ export const zhTW: Record = { "integrations.lazycodexRoles.saveFailed": "無法儲存 {role} 的模型。", "integrations.lazycodexRoles.saved": "{role} 現在使用 {model}。", "integrations.lazycodexRoles.omoAbsent": "{role} 現在使用 {model}。找不到 omo.jsonc,因此只修改了角色檔案。", - "integrations.lazycodexRoles.omoComments": "角色檔案中 {role} 現在使用 {model}。儲存會刪除 omo.jsonc 的註解,因此未修改它;請手動設定其中的 [codex].agents.{role}。", - "integrations.lazycodexRoles.omoInvalid": "角色檔案中 {role} 現在使用 {model}。omo.jsonc 不是包含 [codex].agents 物件的 JSON 物件,因此未修改。", - "integrations.lazycodexRoles.omoFailed": "角色檔案中 {role} 現在使用 {model},但無法寫入 omo.jsonc。", + "integrations.lazycodexRoles.omoComments": "{role} 現在使用 {model} 執行。omo.jsonc 中有儲存後會遺失的註解,因此未修改;請自行設定其中的 [codex].agents.{role}。", + "integrations.lazycodexRoles.omoInvalid": "{role} 現在使用 {model} 執行。omo.jsonc 不是 LazyCodex 可讀取格式的有效 JSON,因此未修改。", + "integrations.lazycodexRoles.omoFailed": "{role} 現在使用 {model} 執行,但無法寫入 omo.jsonc。", "integrations.lazycodexRoles.retryMirror": "重試 omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc 含有註解,因此在這裡儲存只會修改角色檔案。請手動調整 omo.jsonc 中的 [codex].agents 以保持一致。", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc 中有註解,因此在這裡儲存只會更新角色檔案。請自行設定 omo.jsonc 中的 [codex].agents。", "integrations.lazycodexRoles.auto.button": "自動指派", "integrations.lazycodexRoles.auto.running": "正在評估角色…", - "integrations.lazycodexRoles.auto.hint": "以預設模型評估每個角色,並為每個角色提議足夠且最便宜的模型。套用前不會變更任何內容。", + "integrations.lazycodexRoles.auto.hint": "為每個角色建議夠用且最便宜的模型與推理強度。套用之前不會變更任何內容。", "integrations.lazycodexRoles.auto.failed": "無法評估 Codex 代理角色。", "integrations.lazycodexRoles.auto.title": "提議的模型", "integrations.lazycodexRoles.auto.sizedWith": "以 {model} 評估", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index faead6cee93..c545376a2ed 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1778,10 +1778,10 @@ export const zh: Record = { "integrations.semantics.aside": "仅管理此配置文件的 ~/.aside/u//models.json 中的 providers.opencodex。其他提供商保持不变。应用后请完全退出并重新打开 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中添加一个 OpenCodex 提供商条目,让所有已路由的模型出现在 Raycast AI 模型选择器中。需要 Raycast Pro。", "integrations.semantics.omo": "仅管理 omo 的 models.json 中的 providers.opencodex,路径为 ~/.omo/agent,若设置了 OMO_CODING_AGENT_DIR、SENPI_CODING_AGENT_DIR 或 PI_CODING_AGENT_DIR 则以其为准。你的其他提供商保持不变。对新会话生效。", - "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Codex 智能体角色模型", - "integrations.lazycodexRoles.hint": "为每个智能体角色选择 Codex 使用的模型和推理强度。保存时只修改 $CODEX_HOME/agents 中该角色文件的 model 行和 effort 行;只有在 omo.jsonc 可以安全改写时,才会把选择同步到供 LazyCodex 读取的 omo.jsonc。", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) 角色模型", + "integrations.lazycodexRoles.hint": "为每个角色选择模型和推理强度。保存会更新 $CODEX_HOME/agents 中的角色文件,并在可以安全改写时一并更新 LazyCodex 的 omo.jsonc;选“继承推理强度”则由 Codex 决定推理强度。", "integrations.lazycodexRoles.role": "角色", - "integrations.lazycodexRoles.current": "当前模型", + "integrations.lazycodexRoles.current": "当前", "integrations.lazycodexRoles.model": "新模型", "integrations.lazycodexRoles.modelFor": "{role} 的模型", "integrations.lazycodexRoles.effort": "推理强度", @@ -1794,14 +1794,14 @@ export const zh: Record = { "integrations.lazycodexRoles.saveFailed": "无法保存 {role} 的模型。", "integrations.lazycodexRoles.saved": "{role} 现在使用 {model}。", "integrations.lazycodexRoles.omoAbsent": "{role} 现在使用 {model}。未找到 omo.jsonc,因此只修改了角色文件。", - "integrations.lazycodexRoles.omoComments": "角色文件中 {role} 现在使用 {model}。保存会删除 omo.jsonc 的注释,因此未修改它;请手动设置其中的 [codex].agents.{role}。", - "integrations.lazycodexRoles.omoInvalid": "角色文件中 {role} 现在使用 {model}。omo.jsonc 不是包含 [codex].agents 对象的 JSON 对象,因此未修改。", - "integrations.lazycodexRoles.omoFailed": "角色文件中 {role} 现在使用 {model},但无法写入 omo.jsonc。", + "integrations.lazycodexRoles.omoComments": "{role} 现在使用 {model} 运行。omo.jsonc 中有保存后会丢失的注释,因此未修改;请自行设置其中的 [codex].agents.{role}。", + "integrations.lazycodexRoles.omoInvalid": "{role} 现在使用 {model} 运行。omo.jsonc 不是 LazyCodex 可读取格式的有效 JSON,因此未修改。", + "integrations.lazycodexRoles.omoFailed": "{role} 现在使用 {model} 运行,但无法写入 omo.jsonc。", "integrations.lazycodexRoles.retryMirror": "重试 omo.jsonc", - "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc 含有注释,因此在这里保存只会修改角色文件。请手动调整 omo.jsonc 中的 [codex].agents 以保持一致。", + "integrations.lazycodexRoles.omoCommentsState": "omo.jsonc 中有注释,因此在这里保存只会更新角色文件。请自行设置 omo.jsonc 中的 [codex].agents。", "integrations.lazycodexRoles.auto.button": "自动分配", "integrations.lazycodexRoles.auto.running": "正在评估角色…", - "integrations.lazycodexRoles.auto.hint": "用默认模型评估每个角色,并为每个角色建议够用且最便宜的模型。应用之前不会改动任何内容。", + "integrations.lazycodexRoles.auto.hint": "为每个角色推荐够用且最便宜的模型和推理强度。应用之前不会更改任何内容。", "integrations.lazycodexRoles.auto.failed": "无法评估 Codex 代理角色。", "integrations.lazycodexRoles.auto.title": "建议的模型", "integrations.lazycodexRoles.auto.sizedWith": "使用 {model} 评估", diff --git a/gui/tests/lazycodex-role-models.test.tsx b/gui/tests/lazycodex-role-models.test.tsx index 2d8e3b70283..a4f454abe1e 100644 --- a/gui/tests/lazycodex-role-models.test.tsx +++ b/gui/tests/lazycodex-role-models.test.tsx @@ -208,11 +208,11 @@ test("says when omo.jsonc was skipped because of its comments", async () => { rolesBody = { ...(rolesBody as object), omoJsonc: { state: "comments" } }; omoWriteStatus = "skipped_comments"; await mount(); - expect(container.textContent).toContain("omo.jsonc contains comments"); + expect(container.textContent).toContain("omo.jsonc has comments"); await pick("librarian", "gpt-5.5"); await act(async () => { saveButton("librarian").click(); }); await settle(); - expect(container.textContent).toContain("saving would remove its comments"); + expect(container.textContent).toContain("comments a save would remove"); }); test("a failed omo.jsonc mirror can be retried with the same model", async () => { diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 1ce21b6af2a..04b42fc64fb 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -681,7 +681,7 @@ "codex-retained-root-serialization.test.ts": "codex-integration", "codex-routing-cache-affinity-detour.test.ts": "codex-integration", "codex-routing.test.ts": "codex-integration", - "codex-runtime.test.ts": "codex-integration", + "codex-runtime.test.ts": "codex-integration", "codex-clamp-notice.test.ts": "codex-integration", "codex-runtime-wsl-desktop.test.ts": "codex-integration", "codex-service-manager-probe-hardening.test.ts": "codex-integration", "codex-service-manager-probe.test.ts": "codex-integration", @@ -1328,7 +1328,7 @@ "openai-provider-option.test.ts": "adapters/openai", "responses-forward-client-headers.test.ts": "responses", "openai-responses-passthrough.test.ts": "responses", - "responses-main-quota-observation.test.ts": "responses", + "responses-main-quota-observation.test.ts": "responses", "compaction-continuation-note.test.ts": "responses", "compaction-continuation-wire.test.ts": "responses", "external-task-input-repair.test.ts": "responses", "openai-responses-summary-none.test.ts": "responses", "responses-forward-output-cap.test.ts": "responses", diff --git a/src/adapters/openai-responses/passthrough.ts b/src/adapters/openai-responses/passthrough.ts index 3b913b3caec..19a1cefabba 100644 --- a/src/adapters/openai-responses/passthrough.ts +++ b/src/adapters/openai-responses/passthrough.ts @@ -501,6 +501,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): outBody, destinationDecodesNativeCompactionBlob(provider), threadServingIdentityChanged, + parsed._compactionRequest !== true, ), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true, diff --git a/src/adapters/openai-responses/request-strips.ts b/src/adapters/openai-responses/request-strips.ts index 619d1433fc8..429a66cdd8d 100644 --- a/src/adapters/openai-responses/request-strips.ts +++ b/src/adapters/openai-responses/request-strips.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction"; +import { COMPACT_PROMPT, appendCheckpointContinuationNote, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction"; import { debugProviderDiagnostic } from "../../lib/debug"; import { modelInList } from "../../types"; import { isPlainObject } from "./internal"; @@ -260,10 +260,12 @@ export function scrubOcxCompactionItems( body: unknown, destinationDecodesNativeBlob: boolean, threadServingIdentityChanged: boolean, + addContinuationNote = false, ): unknown { if (!isPlainObject(body) || !Array.isArray(body.input)) return body; let changed = false; + let decodedCheckpoint = false; const input = body.input.map(item => { if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item; const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined; @@ -274,6 +276,7 @@ export function scrubOcxCompactionItems( && !threadServingIdentityChanged ) return item; changed = true; + if (decodeCompactionSummary(encrypted) !== null) decodedCheckpoint = true; return { type: "message", role: "user", @@ -281,5 +284,10 @@ export function scrubOcxCompactionItems( }; }); - return changed ? { ...body, input } : body; + if (!changed) return body; + // #6764: a decoded checkpoint is plain user text from here on, so name its role in the + // instructions. Compaction requests skip this; the summarizer must not be told to continue. + return addContinuationNote && decodedCheckpoint + ? { ...body, input, instructions: appendCheckpointContinuationNote(body.instructions) } + : { ...body, input }; } diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index 0e95860e179..0ea8d1f2d60 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -620,8 +620,8 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl removedEfforts: [...clamp.removedEfforts], affectedModels: [...clamp.affectedModels], }; - for (const line of formatClampLogLines(diagnostic)) console.warn(line); - if (!deps.commandCandidates) persistEffortClamp(diagnostic, { configDir: deps.configDir }); + const changed = deps.commandCandidates ? true : persistEffortClamp(diagnostic, { configDir: deps.configDir }); + if (changed) for (const line of formatClampLogLines(diagnostic)) console.warn(line); deps.onEffortClamp?.(diagnostic); } else if (!deps.commandCandidates) { persistEffortClamp(null, { configDir: deps.configDir }); diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 4c737036595..34507ee808d 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -319,9 +319,12 @@ export function setAccountQuotaFromParsed( credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw), }, updatedAt); } - // Legacy rotation keeps its existing carry behavior, but never inherits policy-only + // Rotation shares validated main-window retirement, but never inherits policy-only // evidence that outlived its disk TTL. Policy has a separate, identity-checked base. - const next = mergeAccountQuota(quota, legacyExisting, updatedAt); + const displayQuota: MainPolicyQuotaObservation = isMain && mainWriter && policyQuota?.shortWindowAbsent === true + ? { ...quota, shortWindowAbsent: true } + : quota; + const next = mergeAccountQuota(displayQuota, legacyExisting, updatedAt); accountQuota.set(accountId, next); if (isMain) { if (mainWriter && policyQuota && snapshotHasUsage(policyQuota)) { @@ -440,7 +443,7 @@ function mergeAccountQuota( } if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt; if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds; - } else if (!policyEvidence || quota.shortWindowAbsent !== true) { + } else if (quota.shortWindowAbsent !== true) { // Unknown usage is not a lower reading. Retain the entire known tuple: pairing // its percentage with new metadata would silently extend or shorten its reset. // An elapsed reset is the exception. It describes a window that has already rolled over, @@ -879,14 +882,17 @@ function filterMainPolicyMonthlyQuota( /** * Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages. * A measured long primary, or explicitly absent primary with measured weekly secondary, - * proves replacement only with complete long/null topology. Null supplies no usable evidence. + * proves replacement with long/null topology; an allowed two-window response may omit tertiary. + * Null supplies no usable evidence. */ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null { const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window]; if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null; const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); const [primary, secondary, tertiary] = windows; - // WHAM explicitly reports absent windows as null; omissions cannot prove replacement. + // Primary and secondary must explicitly report their topology. WHAM also sends + // two-window responses without tertiary; an explicit allowed verdict disambiguates those + // from partial observations. A present tertiary still requires measured long/null evidence. // Policy trusts one complete snapshot only when every non-null window is >=24h AND // carries a valid usage reading: a long window without used_percent leaves that // window's usage unknown, and unknown usage must never release a block. @@ -894,7 +900,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu if (quota && (isMeasuredLongWindow(primary) || (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined)) && (secondary === null || isMeasuredLongWindow(secondary)) - && (tertiary === null || isMeasuredLongWindow(tertiary))) { + && (tertiary === null || isMeasuredLongWindow(tertiary) + || (tertiary === undefined && data.rate_limit?.allowed === true))) { return { ...quota, shortWindowAbsent: true }; } return quota; diff --git a/src/codex/runtime.ts b/src/codex/runtime.ts index d7c9fe4f5b5..32b9fb65a0e 100644 --- a/src/codex/runtime.ts +++ b/src/codex/runtime.ts @@ -225,10 +225,11 @@ export function loadLastEffortClamp( } } +/** Return whether a nonempty observation changed, so catalog notices share persistence's identity. */ export function persistEffortClamp( diagnostic: EffortClampDiagnostic | null, deps: ResolveCodexRuntimeDeps = {}, -): void { +): boolean { const configDir = deps.configDir ?? getConfigDir(); const path = codexRuntimeClampStatePath(configDir); if (!diagnostic || diagnostic.removedEfforts.length === 0) { @@ -237,8 +238,16 @@ export function persistEffortClamp( } catch { /* absent is fine */ } - return; + return false; } + // This is an observation, not a heartbeat: rewriting updatedAt on every sync both churns disk + // and makes identical clamps look new after restart. Compare sets, not catalog enumeration order. + const previous = loadLastEffortClamp(deps); + const setKey = (values: readonly string[]) => JSON.stringify([...new Set(values)].sort()); + if (previous && previous.runtimePath === diagnostic.runtimePath + && previous.runtimeVersion === diagnostic.runtimeVersion + && setKey(previous.removedEfforts) === setKey(diagnostic.removedEfforts) + && setKey(previous.affectedModels) === setKey(diagnostic.affectedModels)) return false; const payload: PersistedClampState = { version: 1, updatedAt: new Date((deps.now ?? Date.now)()).toISOString(), @@ -249,6 +258,7 @@ export function persistEffortClamp( }; mkdirSync(configDir, { recursive: true, mode: 0o700 }); atomicWriteFile(path, `${JSON.stringify(payload, null, 2)}\n`); + return true; } export function displayCodexRuntimePath(command: string): string { @@ -1232,6 +1242,5 @@ export function formatClampLogLines(diagnostic: EffortClampDiagnostic): string[] const efforts = diagnostic.removedEfforts.join(", "); return [ `[opencodex] Removed unsupported reasoning efforts: ${efforts}`, - "[opencodex] Run ocx doctor for diagnosis and recovery.", ]; } diff --git a/src/responses/compaction.ts b/src/responses/compaction.ts index 516d92ba333..b3286033ac9 100644 --- a/src/responses/compaction.ts +++ b/src/responses/compaction.ts @@ -56,6 +56,20 @@ export const SUMMARY_PREFIX = "Another language model started to solve this prob export const OPAQUE_COMPACTION_NOTE = "[earlier conversation was compacted; the summary is stored in a format this model cannot read]"; +/** + * Prompt contract for a routed model receiving a decoded `ocx1` checkpoint as user text (#6764). + * It preserves existing authorization scope and permission gates; it grants no authority. + * Kept apart from SUMMARY_PREFIX, which the v1 retention and recovery paths match exactly. + */ +export const CHECKPOINT_CONTINUATION_NOTE = "This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope."; + +/** Append the continuation note to existing instructions once. */ +export function appendCheckpointContinuationNote(instructions: unknown): string { + const existing = typeof instructions === "string" ? instructions : ""; + if (existing.includes(CHECKPOINT_CONTINUATION_NOTE)) return existing; + return existing.trim().length > 0 ? `${existing}\n\n${CHECKPOINT_CONTINUATION_NOTE}` : CHECKPOINT_CONTINUATION_NOTE; +} + /** * Item types in the compact wire family. Each carries an `encrypted_content` blob the client * replays verbatim on every later turn, and the minting backend verifies it is unmodified. diff --git a/src/responses/parser.ts b/src/responses/parser.ts index 236ebcd5db1..9f4c14a84e2 100644 --- a/src/responses/parser.ts +++ b/src/responses/parser.ts @@ -15,7 +15,7 @@ import { createToolChoiceResolver, namespacedToolName } from "../types"; import { responsesRequestSchema } from "./schema"; import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata"; import { lookupReplayThoughtSignature } from "./thought-signature-replay"; -import { compactionItemToText, isCompactionItemType } from "./compaction"; +import { CHECKPOINT_CONTINUATION_NOTE, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "./compaction"; import { previousResponseReplayPrefixLength } from "./state"; import { decodeReasoningEnvelope } from "./reasoning-envelope"; import { hasRoutedIdentity, nameRoutedIdentity } from "../adapters/identity"; @@ -193,6 +193,7 @@ export function parseRequest( // Remote compaction v2: the input tail carries `{type:"compaction_trigger"}` and Codex expects a // synthetic `{type:"compaction"}` output item (src/responses/compaction.ts). Flagged for the server. let compactionRequest = false; + let decodedCheckpoint = false; let contextCompactionBoundary = false; let continuationConversationMessageIndex: number | undefined; @@ -261,6 +262,7 @@ export function parseRequest( if (inputIndex >= replayedInputPrefixLength) contextCompactionBoundary = true; const encrypted = (item as { encrypted_content?: unknown }).encrypted_content; if (effectiveType === "context_compaction" && typeof encrypted !== "string") continue; + if (typeof encrypted === "string" && decodeCompactionSummary(encrypted) !== null) decodedCheckpoint = true; pendingReasoning.length = 0; messages.push({ role: "user", @@ -586,6 +588,10 @@ export function parseRequest( .map(t => loadedToolNames.has(namespacedToolName(t.namespace, t.name)) ? { ...t, loadedFromToolSearch: true } : t); + // #6764: the decoded checkpoint is ordinary user text on this route. Say in the system prompt + // what it is, so a retained earlier request that it reports as completed is not repeated. + // A compaction request is a summarizer turn and must not be told to continue. + if (decodedCheckpoint && !compactionRequest) systemPrompt.push(CHECKPOINT_CONTINUATION_NOTE); const context: OcxContext = { ...(systemPrompt.length > 0 ? { systemPrompt } : {}), messages, diff --git a/structure/INDEX.md b/structure/INDEX.md index 6d7b21f643c..589579e8e54 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -28,6 +28,7 @@ Persisted config, the Codex home it writes into, and the model catalog it publis | [`codex-home.md`](codex-home.md) | CODEX_HOME resolution, the files opencodex manages there, and Codex-home diagnostics. | | [`codex-prompts.md`](codex-prompts.md) | Prompt-layer repair, base import previews and bounded text-probe sources. | | [`catalog.md`](catalog.md) | Shared Codex catalog assembly, account namespaces, pool rotation, and effort ladders. | +| [`catalog-effort-clamp.md`](catalog-effort-clamp.md) | Persisted effort-clamp observation identity and idempotent sync notices. | | [`subagents.md`](subagents.md) | Multi-agent surface mode and subagent roster ordering. | | [`config-proxy.md`](config-proxy.md) | Global proxy activation, start flags, and credential-safe CLI output. | @@ -139,6 +140,8 @@ A source area can be described by more than one doc, because these docs are orga | `src/client/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) | | `src/clients/` | [`clients/integrations.md`](clients/integrations.md) | | `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`codex-prompts.md`](codex-prompts.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | +| `src/codex/catalog/effort.ts` | [`catalog-effort-clamp.md`](catalog-effort-clamp.md) | +| `src/codex/runtime.ts` | [`catalog-effort-clamp.md`](catalog-effort-clamp.md) | | `src/combos/` | [`runtime.md`](runtime.md)
[`providers-and-adapters.md`](providers-and-adapters.md)
[`providers/jev-decision.md`](providers/jev-decision.md) | | `src/companion/` | [`overview.md`](overview.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`companion.md`](companion.md) | | `src/compatibility/` | [`runtime.md`](runtime.md)
[`adapters/compatibility-contracts.md`](adapters/compatibility-contracts.md) | diff --git a/structure/catalog-effort-clamp.md b/structure/catalog-effort-clamp.md new file mode 100644 index 00000000000..b7206016de0 --- /dev/null +++ b/structure/catalog-effort-clamp.md @@ -0,0 +1,22 @@ +# Effort-clamp observations + +`src/codex/catalog/effort.ts` projects each catalog onto the selected Codex runtime's supported +efforts. This projection runs on each sync; its diagnostic is an observation, not a heartbeat. + +`persistEffortClamp` in `src/codex/runtime.ts` compares the existing diagnostic's runtime path and +version and the order-independent sets of removed efforts and affected models before writing. +An unchanged observation preserves file bytes and its timestamp and returns false; a new nonempty +observation is atomically persisted and returns true. An empty or absent diagnostic clears the +record. No schema or on-disk path changes. + +The catalog caller emits the clamp notice only for a changed persisted observation. It still calls +`onEffortClamp` for every nonempty projection. Clearing a clamp allows a later recurrence to notify. +Injected command-candidate fixtures do not persist diagnostics and keep per-call notifications. +The sync clamp notice does not suggest doctor: supported-effort projection is not a broken +installation. Runtime-selection failure and status diagnostics keep their existing behavior. + +`tests/codex-integration/codex-clamp-notice.test.ts` checks unchanged persistence, changed identity +fields, order-independent sets, callbacks, repeat notices, and clearing/recurrence with fake CLI +responses and disposable homes. It does not run the native CLI or verify live client rendering. + +> Decision record: [ADR-7004](decisions/ADR-7004-effort-clamp-observation.md) diff --git a/structure/catalog.md b/structure/catalog.md index 1420dfc00a3..17c0ca93bb5 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -1,6 +1,6 @@ # Model Catalog -Activation-owned metadata discovery no longer refreshes known deadlines merely because quota snapshots age. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Pool 2 discovery uses the [instance credential ownership contract](providers/anthropic-account-pool.md#discovery-credential-ownership). The retained writer uses an observed catalog's nonblocking runtime snapshot for effort diagnostics; it does not run a synchronous version probe. Portable Rust-driven Bun contracts run in `.github/workflows/catalog-async-contracts.yml` on Linux, macOS and Windows. +Activation-owned metadata discovery no longer refreshes known deadlines merely because quota snapshots age. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Pool 2 discovery uses the [instance credential ownership contract](providers/anthropic-account-pool.md#discovery-credential-ownership). The retained writer uses an observed catalog's nonblocking runtime snapshot for effort diagnostics; it does not run a synchronous version probe. Unchanged clamp diagnostics follow the [effort-clamp observation contract](catalog-effort-clamp.md). Portable Rust-driven Bun contracts run in `.github/workflows/catalog-async-contracts.yml` on Linux, macOS and Windows. Native result continuations and function-result injection follow [the mode-specific result and control contract](transports/streaming-health.md#experimental-native-function-result-injection); this surface does not infer upstream support or alter its defaults. Explicit Codex CLI installation observation supplies no selected-runtime proof to catalog discovery or publication. See the [read-only observation contract](runtime.md#explicit-codex-cli-installation-observation). diff --git a/structure/dashboard-and-usage.md b/structure/dashboard-and-usage.md index 3987c7fc2d2..c7d34948aac 100644 --- a/structure/dashboard-and-usage.md +++ b/structure/dashboard-and-usage.md @@ -9,10 +9,10 @@ in [GUI and management API](gui-and-management-api.md). The dashboard is a local control surface, not a separate service. It should reflect the same config and catalog invariants documented in this folder rather than inventing parallel state. -Codex quota cards consume the display cache from `src/codex/quota.ts`. A partial refresh -removes an omitted short tuple whose reset deadline has elapsed, so a stale model-derived -5h row does not persist on a weekly-only account. This is independent of the main-account -blocking hard-lock evidence and reset-notification history; their retention rules are documented in +Codex quota cards consume the display cache from `src/codex/quota.ts`. Partial refreshes remove +omitted short tuples after their reset deadline; validated main-window absence also removes +future-reset short tuples. Main-account hard-lock evidence and reset-notification history follow +separate retention rules in [OpenAI account modes](providers/openai-tiers.md#quota-cache-and-short-window-history). Codex account panels expose no Spark quota toggle or setting and retain quota refresh, pause/resume, selection order, reset-credit confirmation and Advanced settings; account/provider quota DTOs suppress retired windows upstream of the generic quota renderer, under [OpenAI quota scopes](providers/openai-tiers.md#public-provider-contract). diff --git a/structure/decisions/ADR-7003-decoded-checkpoint-continuation.md b/structure/decisions/ADR-7003-decoded-checkpoint-continuation.md new file mode 100644 index 00000000000..1a56c0f7566 --- /dev/null +++ b/structure/decisions/ADR-7003-decoded-checkpoint-continuation.md @@ -0,0 +1,26 @@ +# ADR-7003: decision recorded under "Decoded checkpoint continuation" + +- Purpose: address #6764, where a routed model receives an older request beside a decoded + checkpoint; #7003's Ingwannu review requires explicit authorization limits and outbound evidence. +- Prior constraint: decoded `ocx1:` checkpoints replay as user text; `SUMMARY_PREFIX` is an exact + retention/recovery marker and stays unchanged. A checkpoint's classification of active work + cannot decide whether the user has authorized it or whether a required decision remains pending. +- Alternatives: infer authorization programmatically from checkpoint text, or unconditionally + continue any task it calls active. Both are rejected: checkpoint/delegation text cannot grant + authority, resolve a permission gate, or expand the scope of an existing authorization. +- Choice: retain the existing conditional application and append helper, and use this exact prompt + contract in translated system prompts and passthrough instructions: + +> This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope. + +- Exclusions: compaction/summarizer turns, opaque native blobs and requests without decoded + checkpoints receive no continuation note. No authorization inference or new runtime gate is added. + Existing permission/confirmation gates and later user instructions remain authoritative. +- Consequences: authorized work can continue within its existing scope without repeated permission + requests, while pending decisions, scoped delegations and later pause/cancel/replacement remain + explicit constraints. Checkpoint text, `SUMMARY_PREFIX` and application conditions stay unchanged. +- Evidence boundary: literal prompt-contract fixtures cover those cases through parsing and + passthrough wire construction. Real translated adapter builders cover preservation of existing + instructions and the note, plus negative controls; the coding-agent shared prompt builder is + checked separately. No live model behavior, Cursor/Devin runTurn transport, or per-CLI runTurn + acceptance is established by these construction tests. diff --git a/structure/decisions/ADR-7004-effort-clamp-observation.md b/structure/decisions/ADR-7004-effort-clamp-observation.md new file mode 100644 index 00000000000..e2b63d18662 --- /dev/null +++ b/structure/decisions/ADR-7004-effort-clamp-observation.md @@ -0,0 +1,16 @@ +# ADR-7004: decision recorded under "Effort-clamp observations" + +- Purpose: make repeated unchanged syncs preserve effort-clamp observations instead of producing + fresh timestamps, disk replacements and repeated warnings (#7004). +- Prior constraint: the catalog rebuilds unclamped rows each time; clamping them again is correct, + but unconditional logging and persistence mistake that deterministic projection for a change. +- Alternatives: process-local notice memoization, suppressing all warnings, or comparing the existing + persisted diagnostic. Process memory cannot deduplicate separate CLI invocations; suppressing all + warnings hides new runtime/catalog incompatibilities. +- Choice: reuse the existing diagnostic loader/persister and compare runtime path/version plus + order-independent effort/model sets before writing. Return change information to the caller so + notification and persistence cannot disagree. Preserve every observation callback. +- Consequences: no new files or schema, unchanged observations keep their original timestamp, and + real changes or recurrence after clearing still notify. The existing atomic writer is retained; + this read/compare/write is not cross-process transactional notification deduplication. Remove only + the misleading sync clamp's doctor hint; runtime-selection diagnostics are unchanged. diff --git a/structure/manifest.json b/structure/manifest.json index bfca98fb21f..c23541bae70 100644 --- a/structure/manifest.json +++ b/structure/manifest.json @@ -159,6 +159,13 @@ "src/server/" ] }, + { + "path": "catalog-effort-clamp.md", + "tier": 2, + "title": "Effort-clamp observations", + "scope": "Persisted effort-clamp observation identity and idempotent sync notices.", + "documents": ["src/codex/runtime.ts", "src/codex/catalog/effort.ts"] + }, { "path": "subagents.md", "tier": 2, diff --git a/structure/ops/test-sandbox-cleanup.md b/structure/ops/test-sandbox-cleanup.md index 12d22ffdb8a..531aa18f48b 100644 --- a/structure/ops/test-sandbox-cleanup.md +++ b/structure/ops/test-sandbox-cleanup.md @@ -30,7 +30,8 @@ error, including a foreign AbortError, still fails its case. Callers settle that draining producers/reaps and restoring or removing a home. The helper does not replace fixture-specific cleanup or claim OS ACL coverage for synthetic tests. -`tests/helpers/fixture-teardown.ts` owns management-auth, OAuth live-update, and Anthropic instance +`tests/helpers/fixture-teardown.ts` owns management-auth, OAuth live-update, Codex quota-auto-refresh +case roots (`tests/codex-integration/codex-quota-auto-refresh.test.ts`), and Anthropic instance root removal (`tests/helpers/anthropic-instance-fixture.ts`). It settles producers, then each root's config flights, closes the request-history SQLite index, drains each root's ACL work, and only then removes drained roots. A producer or history failure diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index ff5e6840b6b..9631ceab68e 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -414,18 +414,22 @@ hard-lock tests registered in `scripts/test-layout/layout.json`, including A single fresh valid WHAM response with a measured long primary, or an explicitly null primary and measured secondary that supplies parsed weekly usage, can replace an obsolete short-window tuple. -Secondary and tertiary must each be explicitly null or explicitly long with a valid usage reading. +Secondary must be explicitly null or explicitly long with a valid usage reading. Tertiary follows +the same rule when present; a two-window response may omit it only with `rate_limit.allowed === true`. Long means **at least 24 hours**, matching the parser's short/long discriminator; a one-day primary qualifies, not only a seven-day or monthly window. The policy trusts that one reported topology; it does not require repeated observations or independently confirm upstream window completeness. -Any omitted window field, an unreadable long window, an unknown duration, partial headers, or invalid usage +An omitted primary/secondary, omitted tertiary without that verdict, unreadable long window, unknown duration, partial headers, or invalid usage cannot prove that the short window disappeared. All-null credits-only and tertiary-only Go/Free responses remain insufficient. Replacement proof belongs only to that observation and is never persisted; the resulting weekly/monthly window still blocks at its long threshold (98% by default). This prevents old short-window exhaustion -from surviving indefinitely on a now weekly/monthly account. Coverage lives in +from surviving indefinitely on a now weekly/monthly account. A live main writer applies that same +retirement proof to the display/rotation cache, so account selection cannot keep refusing the +recovered main account on its old short tuple. Coverage lives in `tests/codex-integration/main-quota-evidence-validation.test.ts`, `tests/codex-integration/main-account-hard-lock-retirement.test.ts`, `tests/codex-integration/main-quota-provenance.test.ts`, and -`tests/codex-integration/main-account-hard-lock-recovery.test.ts`. +`tests/codex-integration/main-account-hard-lock-recovery.test.ts`; request admission without credit +consent is covered by `tests/codex-integration/main-account-credit-renewal.test.ts`. The policy reads a separately retained identity-tagged quota snapshot, so the legacy rotation cache's six-hour expiry does not silently release a known block. A confirmed account transition diff --git a/structure/transports/responses-wire-shapes.md b/structure/transports/responses-wire-shapes.md index 5d5b476f0cc..20646910249 100644 --- a/structure/transports/responses-wire-shapes.md +++ b/structure/transports/responses-wire-shapes.md @@ -18,8 +18,8 @@ remain, and the note asks the next model to reopen unresolved visual evidence. P commentary-only or unphased histories stay intact. Sidecar preparation projects parsed messages before vision planning. The projection does not rewrite `_rawBody` or stored history; later vision preprocessing may rewrite the request-local raw body for transport safety. Normal generation and native compaction -stay unchanged. Both routed v1 and v2 pass through this boundary; -raw Responses gateways retain their existing text-only compaction conversion. +stay unchanged. Both routed v1 and v2 pass through this boundary; raw Responses gateways retain their existing text-only compaction conversion. +The [decoded checkpoint continuation contract](responses.md#decoded-checkpoint-continuation) governs the conditional system note and its authorization limits. ## Direct MCP calls in code mode diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 1608a832257..723d5173da7 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -331,23 +331,13 @@ again held its entry for the life of the process. > Decision record: [ADR-0038](../decisions/ADR-0038-responses-http-sse.md) -A replayed compaction item carries an `encrypted_content` blob only its minting backend can decode, -and the client replays it on every later turn. The proxy's own `ocx1:` envelopes are transparent -base64, so they always lower to plain user messages. A native blob is relayed only when there is no -known serving-identity mismatch and the destination is known to decode native blobs — the canonical -ChatGPT forward surface, the official OpenAI API, or a provider with the explicit -`decodesNativeCompactionBlobs` capability. The destination gate alone is insufficient because more -than one backend, including OpenAI and xAI, mints native blobs: a destination can decode its own blob -without being able to decode the previous backend's. The same serving-identity mismatch signal -therefore strips reasoning `encrypted_content` and degrades native compaction blobs through the -existing opaque-note path. Claude-native reasoning adds a per-blob tag check at every route binding: -only the matching durable endpoint and generation-bearing credential retain raw and parsed carriers. Without recorded thread identity, the destination gate remains. Noncanonical forward providers -receive no caller credentials and may point at any backend. On any other routed destination the blob -also degrades to the same opaque note the bridged parser uses, because forwarding it there fails the -turn and the item outlives the failure in the client transcript, repeating on every later turn -including the compaction turn the proxy itself drives. With `store: false`, request sanitization -strips ids from every input item, including compact-wire items, matching codex-rs -(`core/src/client.rs:918-925`). Compact-wire items remain exempt from response-side field backfill. +A replayed compaction item carries an `encrypted_content` blob only its minting backend can decode, and the client replays it on every later turn. The proxy's own `ocx1:` envelopes are transparent base64, so they always lower to plain user messages. A +native blob is relayed only when there is no known serving-identity mismatch and the destination is known to decode native blobs — the canonical ChatGPT forward surface, the official OpenAI API, or a provider with the explicit +`decodesNativeCompactionBlobs` capability. The destination gate alone is insufficient because more than one backend, including OpenAI and xAI, mints native blobs: a destination can decode its own blob without being able to decode the previous backend's. +The same serving-identity mismatch signal therefore strips reasoning `encrypted_content` and degrades native compaction blobs through the existing opaque-note path. Claude-native reasoning adds a per-blob tag check at every route binding: only the +matching durable endpoint and generation-bearing credential retain raw and parsed carriers. Without recorded thread identity, the destination gate remains. Noncanonical forward providers receive no caller credentials and may point at any backend. On any +other routed destination the blob also degrades to the same opaque note the bridged parser uses, because forwarding it there fails the turn and the item outlives the failure in the client transcript, repeating on every later turn including the compaction +turn the proxy itself drives. With `store: false`, request sanitization strips ids from every input item, including compact-wire items, matching codex-rs (`core/src/client.rs:918-925`). Compact-wire items remain exempt from response-side field backfill. For replayed `encrypted_content` slots whose minting provenance is unavailable after a restart or full-history resend, the plaintext-compatibility boundary requires canonical key-independent Fernet @@ -406,6 +396,16 @@ always become unable to serve. > Decision record: [ADR-0039](../decisions/ADR-0039-responses-http-sse.md) +## Decoded checkpoint continuation + +`src/responses/parser.ts` adds `CHECKPOINT_CONTINUATION_NOTE` from `src/responses/compaction.ts` to the system prompt only when normal generation contains a decoded `ocx1:` checkpoint; `src/adapters/openai-responses/request-strips.ts` applies the same condition to passthrough `instructions` while lowering the checkpoint to a user message. Summarizer turns, opaque native blobs and requests without decoded checkpoints receive no note. `SUMMARY_PREFIX` and checkpoint text remain unchanged, and later user messages retain their order after the checkpoint. + +The note is a prompt contract and grants no authority. Completed requests remain history unless a later user message asks to repeat them. Unfinished work continues only under existing authorization and within its scope, without asking for that authorization again while it still applies. Permission and confirmation gates remain authoritative: a pending decision or required permission must resolve before the dependent action. Checkpoints and delegations, including those from another thread, neither grant authority nor expand scope. Later user messages take priority, including pause, cancellation, replacement and scope changes. + +The continuation-note and continuation-wire tests establish prompt-contract and wire-construction evidence, including preservation of existing instructions and checkpoint ordering. They do not establish model behavior or whether a model actually resumes the correct task. + +> Decision record: [ADR-7003](../decisions/ADR-7003-decoded-checkpoint-continuation.md) + ## Core module ownership `src/server/responses/core.ts` is the public ingress and compatibility-export surface. diff --git a/tests/codex-integration/codex-clamp-notice.test.ts b/tests/codex-integration/codex-clamp-notice.test.ts new file mode 100644 index 00000000000..7f02e9f6112 --- /dev/null +++ b/tests/codex-integration/codex-clamp-notice.test.ts @@ -0,0 +1,73 @@ +import { afterEach, expect, spyOn, test } from "bun:test"; +import { mkdtempSync, readFileSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { codexRuntimeClampStatePath, persistEffortClamp, type EffortClampDiagnostic } from "../../src/codex/runtime"; +import { clampCatalogModelsToCodexSupport } from "../../src/codex/catalog/effort"; +import { resetBundledCatalogCacheForTests } from "../../src/codex/catalog/bundled"; +import { resetCodexRuntimeResolveCacheForTests } from "../../src/codex/runtime"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +const roots: string[] = []; +const home = () => { const root = mkdtempSync(join(tmpdir(), "ocx-clamp-notice-")); roots.push(root); return root; }; +const diagnostic = (): EffortClampDiagnostic => ({ runtimePath: "/fixture/codex", runtimeVersion: "0.162.1", + removedEfforts: ["minimal", "xhigh"], affectedModels: ["fixture/a", "fixture/b"] }); +afterEach(() => { + resetBundledCatalogCacheForTests(); + resetCodexRuntimeResolveCacheForTests(); + for (const root of roots.splice(0)) removeTreeWithRetry(root); +}); + +test("unchanged clamp sets preserve file bytes, inode and timestamp across observations", () => { + const configDir = home(); + persistEffortClamp(diagnostic(), { configDir, now: () => 1_800_000_000_000 }); + const path = codexRuntimeClampStatePath(configDir); + const before = readFileSync(path, "utf8"), stat = statSync(path); + const reordered = { ...diagnostic(), removedEfforts: ["xhigh", "minimal", "minimal"], affectedModels: ["fixture/b", "fixture/a"] }; + persistEffortClamp(reordered, { configDir, now: () => 1_800_000_001_000 }); + expect(readFileSync(path, "utf8")).toBe(before); + expect(statSync(path).ino).toBe(stat.ino); + expect(statSync(path).mtimeMs).toBe(stat.mtimeMs); +}); + +test.each(["runtimePath", "runtimeVersion", "removedEfforts", "affectedModels"] as const)( + "a changed %s observation is persisted", field => { + const configDir = home(); + const first = diagnostic(); + persistEffortClamp(first, { configDir, now: () => 1_800_000_000_000 }); + const next = { ...first, [field]: Array.isArray(first[field]) ? ["changed"] : "changed" } as EffortClampDiagnostic; + persistEffortClamp(next, { configDir, now: () => 1_800_000_001_000 }); + const saved = JSON.parse(readFileSync(codexRuntimeClampStatePath(configDir), "utf8")); + expect(saved[field]).toEqual(next[field]); + expect(saved.updatedAt).toBe("2027-01-15T08:00:01.000Z"); + }, +); + +test("catalog clamp warns only on changes, preserves callbacks and can clear then warn again", () => { + const configDir = home(); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + const stderr = spyOn(process.stderr, "write").mockImplementation(() => true); + let callbacks = 0; + const deps = { configDir, platform: "linux" as const, env: { CODEX_CLI_PATH: "/fixture/codex", PATH: "" }, + existsSync: (path: string) => path === "/fixture/codex", + execFileSync: (_file: string, args: readonly string[]) => args.includes("--version") ? "codex-cli 0.162.1" : JSON.stringify({ models: [{ + slug: "gpt-5.5", base_instructions: "fixture", default_reasoning_level: "medium", + supported_reasoning_levels: [{ effort: "low" }, { effort: "medium" }, { effort: "high" }], + }] }), + onEffortClamp: () => { callbacks++; }, + }; + const models = () => [{ slug: "fixture/a", default_reasoning_level: "minimal", + supported_reasoning_levels: [{ effort: "minimal" }, { effort: "low" }, { effort: "high" }] }]; + const notices = () => warn.mock.calls.filter(call => String(call[0]).includes("Removed unsupported reasoning efforts")); + try { + clampCatalogModelsToCodexSupport(models(), deps); + expect(notices()).toHaveLength(1); + clampCatalogModelsToCodexSupport(models(), deps); + expect(notices()).toHaveLength(1); + expect(callbacks).toBe(2); + expect(warn.mock.calls.some(call => String(call[0]).includes("Run ocx doctor"))).toBe(false); + clampCatalogModelsToCodexSupport([], deps); + clampCatalogModelsToCodexSupport(models(), deps); + expect(notices()).toHaveLength(2); + } finally { warn.mockRestore(); stderr.mockRestore(); } +}); diff --git a/tests/codex-integration/codex-quota-auto-refresh.test.ts b/tests/codex-integration/codex-quota-auto-refresh.test.ts index a733fba73c7..63462bec737 100644 --- a/tests/codex-integration/codex-quota-auto-refresh.test.ts +++ b/tests/codex-integration/codex-quota-auto-refresh.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -22,6 +22,7 @@ import type { OcxConfig } from "../../src/types"; import { startupHealthFixture } from "../helpers/startup-health"; import { saveCodexAccountCredential } from "../../src/codex/account-store"; import { clearAccountNeedsReauth, isAccountNeedsReauth } from "../../src/codex/account-runtime-state"; +import { drainAndRemoveFixtureRoots } from "../helpers/fixture-teardown"; const NOW = 1_800_000_000_000; const RESET_SECONDS = NOW / 1000; @@ -114,14 +115,20 @@ beforeEach(() => { resetCodexQuotaAutoRefreshForTests(); }); -afterEach(() => { +afterEach(async () => { globalThis.fetch = previousFetch; clearAccountNeedsReauth("pool-a"); clearAccountQuota(); resetCodexQuotaAutoRefreshForTests(); - if (previousHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousHome; - if (testHome && existsSync(testHome)) rmSync(testHome, { recursive: true, force: true }); + // Windows may still hold credential-hardening handles after a quota assertion. Drain their + // owners before removal; synchronous EBUSY retries alone cannot deliver an async child reap. + await drainAndRemoveFixtureRoots({ + roots: testHome ? [{ path: testHome }] : [], + restoreEnvironment: () => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + }, + }); }); describe("Codex quota window auto refresh", () => { diff --git a/tests/codex-integration/main-account-credit-renewal.test.ts b/tests/codex-integration/main-account-credit-renewal.test.ts index 54beffbe752..10a26d01edc 100644 --- a/tests/codex-integration/main-account-credit-renewal.test.ts +++ b/tests/codex-integration/main-account-credit-renewal.test.ts @@ -9,7 +9,7 @@ import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id"; import { reconcileMainCodexAccountRuntimeState, resetMainCodexAccountIdentityTrackingForTests } from "../../src/codex/account-lifecycle"; import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "../../src/codex/account-runtime-state"; import { captureMainQuotaWriter, clearMainAccountInfoCache, getMainAccountInfoCache, isMainAccountIdentityGenerationLive, observeMainQuotaCredential, setMainAccountInfoCache } from "../../src/codex/main-account-cache"; -import { clearAccountQuota, getMainPolicyQuota, setAccountQuotaFromParsed } from "../../src/codex/quota"; +import { clearAccountQuota, getAccountQuota, getMainPolicyQuota, setAccountQuotaFromParsed } from "../../src/codex/quota"; import { hasSpendableCodexCredits, type CodexSpendableCredits } from "../../src/codex/quota-types"; import { resetQuotaQueryBackoffForTests } from "../../src/codex/quota-query-backoff"; import { clearCodexUpstreamHealth } from "../../src/codex/routing"; @@ -157,6 +157,64 @@ test("without renewal, five-minute credit expiry produces a local 429", async () expect(calls).toEqual([]); }); +test.each([[false, false], [true, false], [false, true], [true, true]] as const)( + "plan upgrade retires an exhausted short window without credit consent (hard lock=%j, tertiary null=%j)", + async (hardLock, tertiaryNull) => { + cfg.codexMainAccountHardLock = hardLock; + cfg.creditCodexAccountIds = []; + clearAccountQuota(MAIN); + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, + shortResetAt: Math.floor((now + HOUR) / 1000), weeklyPercent: 20 }, undefined, + captureMainQuotaWriter(accountId)); + fetchWith(async () => Response.json({ plan_type: "prolite", rate_limit: { + allowed: true, + primary_window: { used_percent: 3, limit_window_seconds: 604_800, + reset_at: Math.floor((now + 7 * 24 * HOUR) / 1000) }, + secondary_window: null, + ...(tertiaryNull ? { tertiary_window: null } : {}), + }, credits: { has_credits: false, unlimited: false, balance: "0" } })); + const result = await fetchMainAccountInfoAttempt(true, 0); + expect(result.info.plan).toBe("prolite"); + expect(result.info.quota?.weeklyPercent).toBe(3); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + await expect(resolveCodexAuthContext(new Headers(), cfg, "pool")).resolves.toMatchObject({ kind: "main-pool" }); + for (const field of ["shortPercent", "shortObservedAt", "shortResetAt", "shortWindowSeconds"] as const) { + expect(getMainPolicyQuota()?.[field]).toBeUndefined(); + expect(getAccountQuota(MAIN)?.[field]).toBeUndefined(); + } + expect(calls).toEqual([whamUrl]); + }, +); + +test.each([false, true])("delayed plan upgrade cannot retire short usage after credential replacement (tertiary null=%j)", async tertiaryNull => { + clearAccountQuota(MAIN); + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, + shortResetAt: Math.floor((now + HOUR) / 1000), weeklyPercent: 20 }, undefined, + captureMainQuotaWriter(accountId)); + const policyBefore = getMainPolicyQuota()!; + const rotationBefore = getAccountQuota(MAIN)!; + expect(rotationBefore.shortResetAt).toBeGreaterThan(now / 1000); + const entered = deferred(); + const finish = deferred(); + fetchWith(async () => { entered.resolve(); return finish.promise; }); + const pending = fetchMainAccountInfoAttempt(true, 0); + await entered.promise; + writeMain(accountId, true); + finish.resolve(Response.json({ plan_type: "prolite", rate_limit: { + allowed: true, primary_window: { used_percent: 3, limit_window_seconds: 604_800 }, + secondary_window: null, ...(tertiaryNull ? { tertiary_window: null } : {}), + } })); + const result = await pending; + expect(result.infoUnpublished).toBe(true); + expect(result.info.quota?.weeklyPercent).toBe(3); + expect(result.freshQuota).toBeUndefined(); + expect(result.resetRecoveryProof).toBeUndefined(); + expect(getMainPolicyQuota()).toEqual(policyBefore); + expect(getAccountQuota(MAIN)).toEqual(rotationBefore); + expect(calls).toEqual([whamUrl]); + expect(getNativeMainProfileRequestCount()).toBe(0); +}); + test.each([false, true])("hidden-dashboard sweep renews at three minutes and admits past original expiry (unlimited=%j)", async unlimited => { const original = now; seed({ hasCredits: true, ...(unlimited ? { unlimited: true } : { balance: 5 }), observedAt: now }); diff --git a/tests/codex-integration/main-account-hard-lock-retirement.test.ts b/tests/codex-integration/main-account-hard-lock-retirement.test.ts index 21db57c6527..f63dd7d913e 100644 --- a/tests/codex-integration/main-account-hard-lock-retirement.test.ts +++ b/tests/codex-integration/main-account-hard-lock-retirement.test.ts @@ -6,7 +6,7 @@ import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id"; import { getMainAccountHardLockStatus } from "../../src/codex/main-account-hard-lock"; import { captureMainQuotaWriter, clearMainAccountInfoCache, observeMainQuotaIdentity } from "../../src/codex/main-account-cache"; import { - applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getMainPolicyQuota, + applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getAccountQuota, getMainPolicyQuota, parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, type WhamUsageResponse, } from "../../src/codex/quota"; import { removeTreeWithRetry } from "../helpers/remove-tree"; @@ -25,7 +25,11 @@ beforeEach(() => { observeMainQuotaIdentity(account); const writer = captureMainQuotaWriter(account)!; const old = Date.now() - 19 * 24 * 60 * 60_000; - writeFileSync(join(home, "codex-quota-cache.json"), JSON.stringify({ version: 1, quotas: {}, + const current = Date.now(); + writeFileSync(join(home, "codex-quota-cache.json"), JSON.stringify({ version: 1, quotas: { + [MAIN]: { shortPercent: 100, shortObservedAt: current, shortResetAt: current / 1000 + 3_600, + shortWindowSeconds: 18_000, weeklyPercent: 0, updatedAt: current }, + }, mainPolicyQuota: { identityKey: writer.identityKey, quota: { shortPercent: 100, shortObservedAt: old, shortResetAt: old / 1000 + 18_000, shortWindowSeconds: 18_000, weeklyPercent: 0, updatedAt: old, @@ -49,6 +53,26 @@ function publish(data: WhamUsageResponse): void { } describe("authoritative main 5h window retirement (#6244)", () => { + for (const primaryAbsent of [false, true]) { + test.each([0, 35, 98, 100])(`allowed two-window snapshot retires short usage in both caches (primary absent=${primaryAbsent}, weekly=%s)`, percent => { + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, + shortResetAt: Date.now() / 1000 + 3_600 }, undefined, captureMainQuotaWriter(account)); + const measured = { ...weekly, used_percent: percent }; + publish({ rate_limit: { allowed: true, + primary_window: primaryAbsent ? null : measured, + secondary_window: primaryAbsent ? measured : null, + } }); + for (const quota of [getAccountQuota(MAIN), getMainPolicyQuota()]) { + expect(quota?.weeklyPercent).toBe(percent); + for (const field of ["shortPercent", "shortObservedAt", "shortResetAt", "shortWindowSeconds"] as const) { + expect(quota?.[field]).toBeUndefined(); + } + expect(quota).not.toHaveProperty("shortWindowAbsent"); + } + expect(getMainAccountHardLockStatus({}).state).toBe(percent < 98 ? "ready" : "blocked"); + }); + } + test.each([0, 35, 97.99, 98, 100])("explicit absent primary retires old 5h while weekly %s still governs", percent => { publish({ rate_limit: { primary_window: null, secondary_window: { ...weekly, used_percent: percent }, tertiary_window: null, @@ -63,6 +87,17 @@ describe("authoritative main 5h window retirement (#6244)", () => { }); test.each([ + { rate_limit: { allowed: false, primary_window: weekly, secondary_window: null } }, + { rate_limit: { allowed: "true", primary_window: weekly, secondary_window: null } }, + { rate_limit: { allowed: true, primary_window: weekly } }, + { rate_limit: { allowed: true, secondary_window: weekly } }, + { rate_limit: { allowed: true, primary_window: {}, secondary_window: weekly } }, + { rate_limit: { allowed: true, primary_window: weekly, secondary_window: {} } }, + { rate_limit: { allowed: true, primary_window: weekly, secondary_window: null, + tertiary_window: { limit_window_seconds: 2_592_000 } } }, + { rate_limit: { allowed: true, primary_window: weekly, secondary_window: null, + tertiary_window: { used_percent: 0, limit_window_seconds: 18_000 } } }, + { rate_limit: { allowed: true, primary_window: { ...weekly, used_percent: 101 }, secondary_window: null } }, { rate_limit: { secondary_window: weekly, tertiary_window: null } }, { rate_limit: { primary_window: null, secondary_window: weekly } }, { rate_limit: { primary_window: null, secondary_window: {}, tertiary_window: null } }, @@ -89,6 +124,22 @@ describe("authoritative main 5h window retirement (#6244)", () => { expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); }); + test.each([101, -1, NaN])("malformed weekly usage %s retains the future-reset rotation tuple and old policy", used_percent => { + const rotationBefore = getAccountQuota(MAIN)!; + const policyBefore = getMainPolicyQuota()!; + expect(rotationBefore.shortResetAt).toBeGreaterThan(Date.now() / 1000); + const data = { rate_limit: { allowed: true, primary_window: { ...weekly, used_percent }, + secondary_window: null } }; + expect(parseMainPolicyUsageQuota(data)).toBeNull(); + publish(data); + expect(getMainPolicyQuota()).toEqual(policyBefore); + expect(getAccountQuota(MAIN)).toMatchObject({ + shortPercent: 100, shortObservedAt: rotationBefore.shortObservedAt, + shortResetAt: rotationBefore.shortResetAt, shortWindowSeconds: 18_000, + }); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); + }); + test.each(["go", "free"])("%s tertiary-only monthly usage cannot prove governing recovery", plan_type => { const data = { plan_type, rate_limit: { primary_window: null, secondary_window: null, tertiary_window: { used_percent: 0, limit_window_seconds: 2_592_000 } } }; @@ -106,13 +157,19 @@ describe("authoritative main 5h window retirement (#6244)", () => { expect(getMainAccountHardLockStatus({}, Date.now() + 30 * 24 * 60 * 60_000).state).toBe("blocked"); }); - test("a stale identity writer cannot publish otherwise authoritative absence", () => { + test.each([false, true])("a stale identity writer cannot publish authoritative absence (tertiary null=%j)", tertiaryNull => { const staleWriter = captureMainQuotaWriter(account)!; + const rotationBefore = getAccountQuota(MAIN)!; + const policyBefore = getMainPolicyQuota()!; + expect(rotationBefore.shortResetAt).toBeGreaterThan(Date.now() / 1000); clearMainAccountInfoCache(); observeMainQuotaIdentity(account); - const data = { rate_limit: { primary_window: null, secondary_window: weekly, tertiary_window: null } }; + const data = { rate_limit: { allowed: true, primary_window: null, secondary_window: weekly, + ...(tertiaryNull ? { tertiary_window: null } : {}) } }; setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data)); expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainPolicyQuota()).toEqual(policyBefore); + expect(getAccountQuota(MAIN)).toEqual(rotationBefore); expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); }); diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index b73680c2e03..d8cb4b4fd65 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -310,10 +310,11 @@ test("window replacement persists without carrying its proof into later partial const publish = (data: WhamUsageResponse) => setAccountQuotaFromParsed( MAIN, parseUsageQuota(data), undefined, writer, parseMainPolicyUsageQuota(data), ); - setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, shortResetAt: 1 }, undefined, writer); + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, + shortResetAt: Date.now() / 1000 + 3_600 }, undefined, writer); expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); publish({ rate_limit: { - primary_window: null, secondary_window: { used_percent: 35, limit_window_seconds: 604_800 }, tertiary_window: null, + allowed: true, primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, } }); // Execute quota's actual debounced serializer through the existing deterministic clock. const persisted = flushPersistence(); @@ -323,6 +324,7 @@ test("window replacement persists without carrying its proof into later partial writeFileSync(join(testDir, "codex-quota-cache.json"), persisted); expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getAccountQuota(MAIN)?.shortPercent).toBeUndefined(); publish({ rate_limit: { primary_window: { used_percent: 99, limit_window_seconds: 18_000 } } }); publish({ rate_limit: { primary_window: { used_percent: 0 } } }); expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index b1647d7ace1..58d33431d9f 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -476,7 +476,7 @@ "codex-restore-app-rewrite.test.ts": "codex-integration", "codex-retained-root-serialization.test.ts": "codex-integration", "codex-routing-cache-affinity-detour.test.ts": "codex-integration", "codex-routing.test.ts": "codex-integration", - "codex-runtime.test.ts": "codex-integration", "codex-runtime-wsl-desktop.test.ts": "codex-integration", + "codex-runtime.test.ts": "codex-integration", "codex-runtime-wsl-desktop.test.ts": "codex-integration", "codex-clamp-notice.test.ts": "codex-integration", "codex-service-manager-probe-hardening.test.ts": "codex-integration", "codex-service-manager-probe.test.ts": "codex-integration", "codex-systemd-env-probe.test.ts": "codex-integration", "codex-shim-autorestore.test.ts": "codex-integration", "codex-shim-destroyed-probe.test.ts": "codex-integration", @@ -850,6 +850,8 @@ "openai-provider-option-tooling.test.ts": "adapters/openai", "openai-provider-option.test.ts": "adapters/openai", "responses-forward-client-headers.test.ts": "responses", "openai-responses-passthrough.test.ts": "responses", "responses-main-quota-observation.test.ts": "responses", + "compaction-continuation-note.test.ts": "responses", + "compaction-continuation-wire.test.ts": "responses", "external-task-input-repair.test.ts": "responses", "openai-responses-summary-none.test.ts": "responses", "responses-forward-output-cap.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", diff --git a/tests/responses/compaction-continuation-note.test.ts b/tests/responses/compaction-continuation-note.test.ts new file mode 100644 index 00000000000..73f05ef8b5f --- /dev/null +++ b/tests/responses/compaction-continuation-note.test.ts @@ -0,0 +1,121 @@ +/** + * Issue #6764: after routed compaction the decoded `ocx1` checkpoint reaches the model as plain + * user text next to the retained earlier request. Both routes add a system-level continuation + * note expressing the authorization limits and priority of later user messages. + * These assertions prove prompt contract and wire construction, not model behavior. + * A compaction request is a summarizer turn and must not receive the note. + */ +import { describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; +import { CHECKPOINT_CONTINUATION_NOTE, SUMMARY_PREFIX, appendCheckpointContinuationNote, encodeCompactionSummary } from "../../src/responses/compaction"; +import { parseRequest } from "../../src/responses/parser"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; + +const EXPECTED_NOTE = "This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope."; + +const checkpoint = { type: "compaction", encrypted_content: encodeCompactionSummary("A done; B active") }; +const oldRequest = { type: "message", role: "user", content: "check example.txt" }; +const NATIVE_BLOB = "gAAAAB-native-opaque-blob"; + +const provider = { adapter: "openai-responses", baseUrl: "https://api.x.ai/v1", authMode: "key" as const, apiKey: "xai-test" }; + +function forwardedBody(rawBody: Record, extra: Record = {}): Record { + const request = withTestTranslatorBudget(createResponsesPassthroughAdapter(provider)).buildRequest({ + modelId: "grok-4.6", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { model: "grok-4.6", store: false, ...rawBody }, + ...extra, + }, { headers: new Headers({ authorization: "Bearer token" }) }); + return JSON.parse(request.body) as Record; +} + +describe("checkpoint continuation prompt contract (#6764)", () => { + test("prompt contract matches the reviewed literal wording", () => { + expect(CHECKPOINT_CONTINUATION_NOTE).toBe(EXPECTED_NOTE); + }); + + const fixtures = [ + { name: "authorized continuation", summary: "A is complete. The user authorized implementing B; B is unfinished and within that scope." }, + { name: "active but awaiting a decision", summary: "B is active, but the user must choose the deployment destination before deployment." }, + { name: "scoped read-only delegation", summary: "Another thread delegated a read-only review of B. Inspect and report only; do not edit or deploy." }, + { name: "delegation awaiting permission", summary: "Another thread delegated B, but the required user permission is pending. Wait before changing files." }, + { name: "later user pause", summary: "A is complete; authorized B is unfinished.", later: "Pause B until I return." }, + { name: "later user cancel", summary: "A is complete; authorized B is unfinished.", later: "Cancel B." }, + { name: "later user replace", summary: "A is complete; authorized B is unfinished.", later: "Replace B with a read-only review of C." }, + ]; + + for (const { name, summary, later } of fixtures) { + test(`prompt contract / wire construction: ${name}`, () => { + const input = [oldRequest, { type: "compaction", encrypted_content: encodeCompactionSummary(summary) }, + ...(later ? [{ type: "message", role: "user", content: later }] : [])]; + const raw = { model: "m", instructions: "Be brief.", input }; + const parsed = parseRequest(raw); + expect(parsed.context.systemPrompt?.at(-1)).toBe(EXPECTED_NOTE); + expect(parsed.context.systemPrompt?.[0]).toContain("Be brief."); + expect(parsed.context.messages[1]?.content).toBe(`${SUMMARY_PREFIX}\n\n${summary}`); + expect(parsed.context.messages).toHaveLength(input.length); + const body = forwardedBody(raw); + expect(body.instructions).toBe(`Be brief.\n\n${EXPECTED_NOTE}`); + const forwarded = body.input as Record[]; + expect(forwarded[1]).toEqual({ type: "message", role: "user", + content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\n${summary}` }] }); + expect(forwarded).toHaveLength(input.length); + if (later) { + expect(parsed.context.messages[2]).toMatchObject({ role: "user", content: later }); + expect(forwarded[2]).toEqual(input[2]); + } + }); + } + test("wire construction: translated route adds the note to the system prompt after a decoded checkpoint", () => { + const parsed = parseRequest({ model: "anthropic/claude-sonnet-4-6", instructions: "Be brief.", input: [oldRequest, checkpoint] }); + expect(parsed.context.systemPrompt?.at(-1)).toBe(EXPECTED_NOTE); + expect(parsed.context.systemPrompt?.[0]).toContain("Be brief."); + // The checkpoint text itself is unchanged, so v1 recovery that matches SUMMARY_PREFIX still works. + expect(parsed.context.messages[1]?.content).toBe(`${SUMMARY_PREFIX}\n\nA done; B active`); + }); + + test("wire construction: translated route leaves requests without a decoded checkpoint alone", () => { + for (const input of [ + [oldRequest], + [oldRequest, { type: "compaction", encrypted_content: NATIVE_BLOB }], + [oldRequest, { type: "context_compaction" }], + ]) { + expect(parseRequest({ model: "m", input }).context.systemPrompt ?? []).toEqual([]); + } + }); + + test("wire construction: a compaction request is a summarizer turn and gets no note", () => { + const parsed = parseRequest({ model: "m", input: [oldRequest, checkpoint, { type: "compaction_trigger" }] }); + expect(parsed._compactionRequest).toBe(true); + expect(parsed.context.systemPrompt ?? []).toEqual([]); + }); + + test("wire construction: passthrough appends the note to instructions once and lowers the checkpoint", () => { + const body = forwardedBody({ instructions: "Be brief.", input: [oldRequest, checkpoint] }); + expect(body.instructions).toBe(`Be brief.\n\n${EXPECTED_NOTE}`); + expect((body.input as Record[])[1]).toEqual({ + type: "message", + role: "user", + content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\nA done; B active` }], + }); + expect(forwardedBody({ input: [oldRequest, checkpoint] }).instructions).toBe(EXPECTED_NOTE); + expect(appendCheckpointContinuationNote(body.instructions)).toBe(body.instructions as string); + }); + + test("wire construction: passthrough keeps instructions untouched without a decoded checkpoint", () => { + expect(forwardedBody({ instructions: "Be brief.", input: [oldRequest] }).instructions).toBe("Be brief."); + expect(forwardedBody({ instructions: "Be brief.", input: [oldRequest, { type: "compaction", encrypted_content: NATIVE_BLOB }] }).instructions) + .toBe("Be brief."); + expect("instructions" in forwardedBody({ input: [oldRequest] })).toBe(false); + }); + + test("wire construction: passthrough compaction request keeps its summarizer instructions", () => { + const body = forwardedBody( + { instructions: "Be brief.", input: [oldRequest, checkpoint, { type: "compaction_trigger" }] }, + { _compactionRequest: true }, + ); + expect(body.instructions).toBe("Be brief."); + }); +}); diff --git a/tests/responses/compaction-continuation-wire.test.ts b/tests/responses/compaction-continuation-wire.test.ts new file mode 100644 index 00000000000..a6b1aab0861 --- /dev/null +++ b/tests/responses/compaction-continuation-wire.test.ts @@ -0,0 +1,143 @@ +/** Synthetic outbound construction only; no model/provider acceptance is established. */ +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createAnthropicAdapter } from "../../src/adapters/anthropic"; +import type { ProviderAdapter } from "../../src/adapters/base"; +import { buildSystemPrompt } from "../../src/adapters/coding-agent/protocol"; +import { createCommandCodeAdapter } from "../../src/adapters/command-code"; +import { createGoogleAdapter } from "../../src/adapters/google"; +import { createKiroAdapter } from "../../src/adapters/kiro"; +import { createOllamaNativeAdapter } from "../../src/adapters/ollama-native"; +import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; +import { CHECKPOINT_CONTINUATION_NOTE, encodeCompactionSummary } from "../../src/responses/compaction"; +import { parseRequest } from "../../src/responses/parser"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; + +const SENTINEL = "Existing instruction sentinel."; +const OLD_REQUEST = "Old user request sentinel."; +const oldRequest = { type: "message", role: "user", content: OLD_REQUEST }; +const checkpoint = { type: "compaction", encrypted_content: encodeCompactionSummary("A complete; authorized B unfinished.") }; +const controls = [ + { name: "no checkpoint", input: [oldRequest] }, + { name: "opaque native blob", input: [oldRequest, { type: "compaction", encrypted_content: "gAAAAB-native-opaque-blob" }] }, + { name: "compaction/summarizer turn", input: [oldRequest, checkpoint, { type: "compaction_trigger" }] }, +]; + +type SystemInstruction = { parts: Array<{ text: string }> }; +type KiroUserEntry = { userInputMessage?: { content: string } }; +interface WireBody { + messages: Array<{ role: string; content: string }>; + system: Array<{ text: string }>; + systemInstruction: SystemInstruction; + request: { systemInstruction: SystemInstruction }; + params: { system: string }; + conversationState: { history?: KiroUserEntry[]; currentMessage: KiroUserEntry }; +} + +const messageSystem = (body: WireBody): string => body.messages + .filter(message => message.role === "system" || message.role === "developer") + .map(message => message.content).join("\n\n"); +const googleSystem = (body: WireBody): string => body.systemInstruction.parts.map(part => part.text).join("\n"); +const syntheticKey = "synthetic-wire-key"; +const routes: Array<{ name: string; model: string; create: () => ProviderAdapter; system: (body: WireBody) => string }> = [ + { name: "OpenAI Chat", model: "local-model", create: () => createOpenAIChatAdapter({ + adapter: "openai-chat", baseUrl: "https://chat.example.invalid/v1", apiKey: syntheticKey, + }), system: messageSystem }, + { name: "Anthropic", model: "claude-sonnet-4-6", create: () => createAnthropicAdapter({ + adapter: "anthropic", baseUrl: "https://api.anthropic.com", authMode: "key", apiKey: syntheticKey, + }), system: body => body.system.map(part => part.text).join("\n") }, + { name: "Google AI Studio", model: "gemini-3-pro", create: () => createGoogleAdapter({ + adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", apiKey: syntheticKey, + }), system: googleSystem }, + { name: "Google Vertex", model: "gemini-3-pro", create: () => createGoogleAdapter({ + adapter: "google", baseUrl: "https://aiplatform.googleapis.com", googleMode: "vertex", + apiKey: syntheticKey, project: "synthetic-project", location: "global", + }), system: googleSystem }, + { name: "Cloud Code Assist", model: "gemini-3-pro", create: () => createGoogleAdapter({ + adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", googleMode: "cloud-code-assist", + apiKey: "synthetic-oauth-token", project: "synthetic-project", + }), system: body => body.request.systemInstruction.parts.map(part => part.text).join("\n") }, + { name: "Kiro", model: "claude-sonnet-4.5", create: () => createKiroAdapter({ + adapter: "kiro", baseUrl: "https://runtime.us-east-1.kiro.dev", authMode: "key", apiKey: "ksk_synthetic-wire-key", + }), system: body => { + // Kiro puts system instructions before the first user's text, rather than in a separate field. + const state = body.conversationState; + const content = state.history?.find(entry => entry.userInputMessage)?.userInputMessage?.content + ?? state.currentMessage.userInputMessage?.content; + expect(content).toBeDefined(); + const boundary = content!.indexOf(OLD_REQUEST); + expect(boundary).toBeGreaterThanOrEqual(0); + return content!.slice(0, boundary); + } }, + { name: "Ollama native", model: "glm-5.3-flash", create: () => createOllamaNativeAdapter({ + adapter: "ollama-native", baseUrl: "https://ollama.com", authMode: "key", apiKey: syntheticKey, + }), system: messageSystem }, + { name: "Command Code", model: "deepseek/deepseek-v4-flash", create: () => createCommandCodeAdapter({ + adapter: "command-code", baseUrl: "https://api.commandcode.ai", authMode: "oauth", apiKey: syntheticKey, + projectContext: "off", + }), system: body => body.params.system }, +]; + +let fixtureHome: string; +let previousHome: string | undefined; +let fetchGuard: ReturnType>; +beforeEach(() => { + previousHome = process.env.OPENCODEX_HOME; + fixtureHome = mkdtempSync(join(tmpdir(), "ocx-checkpoint-wire-")); + process.env.OPENCODEX_HOME = fixtureHome; + fetchGuard = spyOn(globalThis, "fetch").mockImplementation(() => { + throw new Error("wire construction must not use network or ADC"); + }); +}); +afterEach(() => { + fetchGuard.mockRestore(); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(fixtureHome); +}); + +function parsed(model: string, input: unknown[]) { + const request = parseRequest({ model, instructions: SENTINEL, input }); + // Explicit synthetic metadata avoids consulting imported Kiro credentials. + request._kiroAuthContext = { apiRegion: "us-east-1" }; + return request; +} + +describe("checkpoint continuation wire construction", () => { + for (const route of routes) { + test(`${route.name}: system field retains instructions and the prompt contract`, async () => { + const request = await withTestTranslatorBudget(route.create()).buildRequest(parsed(route.model, [oldRequest, checkpoint])); + const system = route.system(JSON.parse(request.body) as WireBody); + expect(system).toContain(SENTINEL); + expect(system).toContain(CHECKPOINT_CONTINUATION_NOTE); + expect(fetchGuard).not.toHaveBeenCalled(); + }); + for (const control of controls) { + test(`${route.name}: wire construction excludes the note for ${control.name}`, async () => { + const request = await withTestTranslatorBudget(route.create()).buildRequest(parsed(route.model, control.input)); + const system = route.system(JSON.parse(request.body) as WireBody); + expect(system).toContain(SENTINEL); + expect(system).not.toContain(CHECKPOINT_CONTINUATION_NOTE); + expect(fetchGuard).not.toHaveBeenCalled(); + }); + } + } + + // Cursor and Devin runTurn routes are not covered. Claude CLI, CodeBuddy and Qoder + // per-CLI runTurn routes are also not covered by this shared prompt-construction assertion. + test("coding-agent prompt contract: shared builder retains parsed instructions and note", () => { + const system = buildSystemPrompt(parsed("coding-model", [oldRequest, checkpoint])); + expect(system).toContain(SENTINEL); + expect(system).toContain(CHECKPOINT_CONTINUATION_NOTE); + }); + for (const control of controls) { + test(`coding-agent prompt contract: shared builder excludes the note for ${control.name}`, () => { + const system = buildSystemPrompt(parsed("coding-model", control.input)); + expect(system).toContain(SENTINEL); + expect(system).not.toContain(CHECKPOINT_CONTINUATION_NOTE); + }); + } +});