Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions gui/src/components/codex-set/PromptLayerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,11 @@ export default function PromptLayerDialog({
? t("codexSet.dialog.emptySource", { path: text.sourcePath ?? "" })
: text?.reason === "not-rendered"
? t("codexSet.dialog.notRendered")
: text?.reason === "not-exposed"
? t("codexSet.dialog.notExposed")
: t("codexSet.dialog.textUnavailable")}
: text?.reason === "unmapped"
? t("codexSet.dialog.unmapped")
: text?.reason === "not-exposed"
? t("codexSet.dialog.notExposed")
: t("codexSet.dialog.textUnavailable")}
</p>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,7 @@ export const de: Record<TKey, string> = {
"codexSet.dialog.notRendered": "In der gelesenen Runde hat diese Ebene nichts gesendet. Abschnitte werden nur bei Änderungen erneut gesendet, daher kann eine einzelne Stichprobe sie nicht enthalten.",
"codexSet.dialog.emptySource": "Die Datei unter {path} existiert, ist aber leer, daher sendet diese Ebene nichts.",
"codexSet.dialog.notExposed": "Der Basis-Prompt läuft außerhalb der Nachrichtenliste, die Codex ausgeben kann, und lässt sich hier nicht anzeigen. Ersetzen ist über model_instructions_file möglich.",
"codexSet.dialog.unmapped": "Für diese Ebene gibt es noch keine bestätigte Zuordnung in der Prompt-Ausgabe, daher kann ihr Text hier noch nicht angezeigt werden.",
"codexSet.dialog.textUnavailable": "Der Codex-Prompt konnte auf diesem Rechner nicht gelesen werden, daher ist der Text nicht verfügbar.",
"codexSet.class.base": "Basisanweisungen",
"codexSet.class.config-toggle": "Hier umschaltbar",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,7 @@ export const en = {
"codexSet.dialog.notRendered": "This layer sent nothing on the turn we read. Sections are only re-sent when they change, so an unchanged layer is absent from a single sample.",
"codexSet.dialog.emptySource": "The file at {path} exists but is empty, so this layer sends nothing.",
"codexSet.dialog.notExposed": "The base prompt travels outside the message list Codex can print, so it cannot be shown here. Replacing it is possible through model_instructions_file.",
"codexSet.dialog.unmapped": "This layer has no confirmed mapping in the prompt output, so its text cannot be shown here yet.",
"codexSet.dialog.textUnavailable": "The Codex prompt could not be read on this machine, so the text is unavailable.",
"codexSet.class.base": "Base instructions",
"codexSet.class.config-toggle": "Switchable here",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@ export const fr: Record<TKey, string> = {
"codexSet.dialog.notRendered": "Lors du tour que nous avons lu, cette couche n'a rien envoyé. Les sections ne sont renvoyées qu'en cas de changement : un seul échantillon peut donc ne pas la contenir.",
"codexSet.dialog.emptySource": "Le fichier {path} existe mais il est vide : cette couche n'envoie donc rien.",
"codexSet.dialog.notExposed": "L'invite de base circule en dehors de la liste de messages que Codex peut afficher ; elle ne peut donc pas être montrée ici. On peut la remplacer via model_instructions_file.",
"codexSet.dialog.unmapped": "Cette couche n'a pas encore de correspondance confirmée dans la sortie de l'invite ; son texte ne peut donc pas encore être affiché ici.",
"codexSet.dialog.textUnavailable": "L'invite Codex n'a pas pu être lue sur cette machine, le texte est donc indisponible.",
"codexSet.class.base": "Instructions de base",
"codexSet.class.config-toggle": "Modifiable ici",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,7 @@ export const ja: Record<TKey, string> = {
"codexSet.dialog.notRendered": "確認したターンでは、このレイヤーは何も送信していません。各セクションは内容が変わったときだけ再送されるため、一度の確認では欠けて見えることがあります。",
"codexSet.dialog.emptySource": "{path} のファイルは存在しますが空のため、このレイヤーは何も送信しません。",
"codexSet.dialog.notExposed": "基本プロンプトは Codex が出力するメッセージ一覧の外を通るため、ここには表示できません。model_instructions_file で置き換えることは可能です。",
"codexSet.dialog.unmapped": "このレイヤーにはプロンプト出力内で確認済みの対応付けがまだないため、ここにはテキストを表示できません。",
"codexSet.dialog.textUnavailable": "このマシンでは Codex のプロンプトを読み取れず、原文を表示できません。",
"codexSet.class.base": "基本指示",
"codexSet.class.config-toggle": "ここで切り替え可能",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ export const ko: Record<TKey, string> = {
"codexSet.dialog.notRendered": "확인한 턴에서는 이 레이어가 아무것도 보내지 않았습니다. 각 섹션은 내용이 바뀔 때만 다시 전송되므로, 한 번 확인한 것만으로는 빠져 보일 수 있습니다.",
"codexSet.dialog.emptySource": "{path} 파일은 있지만 비어 있어서 이 레이어는 아무것도 보내지 않습니다.",
"codexSet.dialog.notExposed": "기본 프롬프트는 Codex가 출력하는 메시지 목록 밖으로 전달되어 여기서 보여줄 수 없습니다. 대신 model_instructions_file로 교체할 수 있습니다.",
"codexSet.dialog.unmapped": "이 레이어는 프롬프트 출력에서 확인된 매핑이 아직 없어 여기에서 텍스트를 표시할 수 없습니다.",
"codexSet.dialog.textUnavailable": "이 컴퓨터에서 Codex 프롬프트를 읽지 못해 원문을 표시할 수 없습니다.",
"codexSet.class.base": "기본 지침",
"codexSet.class.config-toggle": "여기서 전환 가능",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,7 @@ export const ru: Record<TKey, string> = {
"codexSet.dialog.notRendered": "На прочитанном нами шаге этот слой ничего не отправил. Разделы пересылаются только при изменении, поэтому в одной выборке слой может отсутствовать.",
"codexSet.dialog.emptySource": "Файл {path} существует, но пуст, поэтому этот слой ничего не отправляет.",
"codexSet.dialog.notExposed": "Базовый промпт передаётся вне списка сообщений, который Codex может напечатать, поэтому показать его здесь нельзя. Заменить его можно через model_instructions_file.",
"codexSet.dialog.unmapped": "Для этого слоя пока нет подтверждённого сопоставления в выводе промпта, поэтому его текст ещё нельзя показать здесь.",
"codexSet.dialog.textUnavailable": "На этой машине не удалось прочитать промпт Codex, поэтому текст недоступен.",
"codexSet.class.base": "Базовые инструкции",
"codexSet.class.config-toggle": "Переключается здесь",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,7 @@ export const tr: Record<TKey, string> = {
"codexSet.dialog.notRendered": "Okuduğumuz turda bu katman hiçbir şey göndermedi. Bölümler yalnızca değiştiklerinde yeniden gönderilir, bu yüzden tek bir örnekte görünmeyebilir.",
"codexSet.dialog.emptySource": "{path} konumundaki dosya var ancak boş, bu yüzden bu katman hiçbir şey göndermiyor.",
"codexSet.dialog.notExposed": "Temel istem, Codex'in yazdırabildiği mesaj listesinin dışından geçtiği için burada gösterilemez. model_instructions_file ile değiştirilebilir.",
"codexSet.dialog.unmapped": "Bu katmanın istem çıktısında henüz doğrulanmış bir eşlemesi yok, bu nedenle metni burada henüz gösterilemiyor.",
"codexSet.dialog.textUnavailable": "Bu makinede Codex istemi okunamadı, bu yüzden metin kullanılamıyor.",
"codexSet.class.base": "Temel talimatlar",
"codexSet.class.config-toggle": "Buradan değiştirilebilir",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@ export const vi: Record<TKey, string> = {
"codexSet.dialog.sourceBytes": "{bytes} byte",
"codexSet.dialog.emptySource": "Tệp tại {path} tồn tại nhưng không có dữ liệu, vì vậy layer này không gửi đi nội dung nào cả.",
"codexSet.dialog.notExposed": "Base prompt di chuyển bên ngoài danh sách tin nhắn mà Codex có thể in ra, vì vậy không thể hiển thị ở đây. Tuy nhiên có thể thay thế nó thông qua model_instructions_file.",
"codexSet.dialog.unmapped": "Lớp này chưa có ánh xạ được xác nhận trong đầu ra prompt, vì vậy chưa thể hiển thị văn bản của nó ở đây.",
"codexSet.dialog.textUnavailable": "Không thể đọc prompt Codex trên máy này, do đó văn bản không khả dụng.",
"codexSet.class.base": "Base instructions",
"codexSet.class.config-toggle": "Có thể chuyển đổi tại đây",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,7 @@ export const zhTW: Record<TKey, string> = {
"codexSet.dialog.notRendered": "在我們讀取的那一輪中,此層沒有傳送任何內容。各區段只在內容變更時才會重新傳送,因此單次取樣可能看不到它。",
"codexSet.dialog.emptySource": "{path} 檔案存在但為空,因此此層不會傳送任何內容。",
"codexSet.dialog.notExposed": "基礎提示詞不在 Codex 可列印的訊息清單中傳遞,因此無法在此顯示。可以透過 model_instructions_file 取代它。",
"codexSet.dialog.unmapped": "此層在提示詞輸出中尚無已確認的對應,因此暫時無法在此顯示其文字。",
"codexSet.dialog.textUnavailable": "本機無法讀取 Codex 提示詞,因此無法顯示原文。",
"codexSet.class.base": "基礎指令",
"codexSet.class.config-toggle": "可在此切換",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,7 @@ export const zh: Record<TKey, string> = {
"codexSet.dialog.notRendered": "在我们读取的那一轮中,此层没有发送任何内容。各部分仅在内容变化时才会重新发送,因此单次采样可能看不到它。",
"codexSet.dialog.emptySource": "{path} 文件存在但为空,因此此层不会发送任何内容。",
"codexSet.dialog.notExposed": "基础提示词不在 Codex 可打印的消息列表中传递,因此无法在此显示。可以通过 model_instructions_file 替换它。",
"codexSet.dialog.unmapped": "此层在提示词输出中尚无已确认的映射,因此暂时无法在此显示其文本。",
"codexSet.dialog.textUnavailable": "本机无法读取 Codex 提示词,因此无法显示原文。",
"codexSet.class.base": "基础指令",
"codexSet.class.config-toggle": "可在此切换",
Expand Down
20 changes: 20 additions & 0 deletions gui/tests/codex-set-prompt-layers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,26 @@ test("9. the dialog names WHY text is missing rather than omitting it silently",
await act(async () => { root.unmount(); });
});

test("9b. an unmapped layer does not receive the base-prompt explanation", async () => {
// A probe that succeeded but has no confirmed tag for a layer is neither
// "unavailable" nor the base prompt's "not-exposed": the not-exposed copy
// names the base prompt and model_instructions_file, which would mislead an
// operator reading it for an unrelated layer.
stubRoutes(call => call.url.endsWith("/api/codex-prompt/text")
? json({ ok: true, layers: { personality: { text: null, reason: "unmapped", bytes: 0 } } })
: json(snapshot()));
const { container, root } = await mount();
await act(async () => {
(row(container, "personality")!.querySelector("button") as HTMLButtonElement).click();
});
const dialog = document.querySelector("dialog.modal-overlay")!;
const notice = dialog.querySelector(".codex-set-layer-dialog__no-text")!;
expect(notice.textContent).toContain("no confirmed mapping");
expect(notice.textContent).not.toContain("base prompt");
expect(notice.textContent).not.toContain("model_instructions_file");
await act(async () => { root.unmount(); });
});

test("the prompt-text request is aborted when the panel unmounts", async () => {
let textSignal: AbortSignal | null = null;
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
Expand Down
15 changes: 9 additions & 6 deletions src/codex/prompt-text-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const UNMAPPED_LAYER_IDS = [
// The Rust source names a <git_attribution> marker pair, but a world-state section is
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
// no attribution text. Listing the id here reports "not exposed" honestly instead of
// no attribution text. Listing the id here reports "unmapped" honestly instead of
// claiming a tag this extractor has never actually matched - the same mistake the
// header above records for permissions.
"git-attribution",
Expand All @@ -85,7 +85,7 @@ export interface LayerText {
/** Rendered text, when this layer produced a section on the probed turn. */
text: string | null;
/** Why the text is absent, when it is. */
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unmapped" | "unavailable";
bytes: number;
/**
* `expanded` is text Codex sends as written. `template` is a catalog
Expand All @@ -103,7 +103,7 @@ export interface LayerText {

/**
* Why the base prompt is or is not readable, at the granularity a reader can act
* on. `LayerText.reason` has five coarse values and cannot express any of this,
* on. `LayerText.reason` has six coarse values and cannot express any of this,
* which is why the detailed answer travels on its own record.
*/
export type BasePromptReason =
Expand Down Expand Up @@ -374,7 +374,7 @@ function readBasePrompt(codexHome: string): BasePromptText {
/**
* Project the base prompt onto the legacy `base-instructions` layer slot.
*
* The slot is lossy by construction - five coarse reasons, no renderer that reads
* The slot is lossy by construction - six coarse reasons, no renderer that reads
* `representation` - so it carries only what it can carry honestly: published text
* when the source is text Codex sends, and otherwise no text at all. A template is
* deliberately NOT `ok` here, because the dialog labels every `ok` layer "Text
Expand Down Expand Up @@ -944,9 +944,12 @@ export async function probePromptText(

// Layers whose rendered tag we have not confirmed against live output. Leaving
// them absent made the GUI fall through to "unavailable", which claims the probe
// failed when it succeeded. Saying we have no mapping is the smaller claim.
// failed when it succeeded. Keep this distinct from the base prompt's
// "not-exposed", which is confirmed to travel outside the printable message
// list - reusing it showed a base-prompt-specific explanation for unrelated
// layers.
for (const id of UNMAPPED_LAYER_IDS) {
layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
layers[id] ??= { text: null, reason: "unmapped", bytes: 0 };
}
return { ok: true, codexHome, layers, base, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
}
Expand Down
41 changes: 41 additions & 0 deletions tests/codex-integration/codex-prompt-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1520,5 +1520,46 @@ describe("020 coverage completions", () => {
expect(res.body.code).toBe("invalid_body");
});

test("41. unmapped layers stay distinct from the unprintable base prompt", async () => {
// "not-exposed" is the base prompt's contract: it is confirmed to travel
// outside the printable message list, and the GUI renders a
// base-prompt-specific explanation for it. Reusing that reason for layers
// whose tag the extractor has not verified showed that explanation on
// unrelated layers.
const fx = fixture("");
// The probe reads the base prompt from CODEX_HOME - the decoy here, whose
// sentinel config selects model "sentinel". A catalog row that publishes
// only an instructions_template makes the base confirmed unprintable, so
// base-instructions must report "not-exposed" while the tag-less layers
// report "unmapped".
writeFileSync(join(fx.decoyHome, "opencodex-catalog.json"), JSON.stringify({
models: [{ slug: "sentinel", model_messages: { instructions_template: "template {{unprintable}}" } }],
}), "utf8");
const probeOutput = JSON.stringify([{
type: "message",
role: "developer",
content: [{ type: "input_text", text: "<skills_instructions>Skill text.</skills_instructions>" }],
}]);
setPromptTextProbeCommandForTests({
binary: process.execPath,
args: ["-e", `process.stdout.write(${JSON.stringify(probeOutput)});`],
});

const res = await call("GET", "/api/codex-prompt/text", fx);
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
expect(res.body.base.representation).toBe("template");
expect(res.body.layers["base-instructions"].reason).toBe("not-exposed");
// Mirrors UNMAPPED_LAYER_IDS in prompt-text-probe.ts.
for (const id of [
"model-switch", "context-window-guidance", "environments-instructions",
"tools", "multi-agent-mode", "personality", "realtime", "collaboration",
"git-attribution",
]) {
expect(res.body.layers[id]?.reason).toBe("unmapped");
}
expectDecoyUntouched(fx);
});


});
32 changes: 32 additions & 0 deletions tests/codex-integration/codex-prompt-text-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,38 @@ describe("prompt probe process lifecycle", () => {
});
});

describe("unmapped layers", () => {
test("a layer with no confirmed tag reports unmapped, not the base prompt's not-exposed", async () => {
// UNMAPPED_LAYER_IDS used to reuse "not-exposed", which is the base prompt's
// contract: the GUI renders a base-prompt-specific explanation for it. A
// layer the extractor simply has no verified tag for is a smaller claim.
const home = promptHome({
"config.toml": "model = \"gpt-test\"\n",
"opencodex-catalog.json": catalogJson([{ slug: "gpt-test", base_instructions: "Base prompt body." }]),
});
const previousHome = process.env.CODEX_HOME;
process.env.CODEX_HOME = home;
setPromptTextProbeCommandForTests({
binary: process.execPath,
args: ["-e", `process.stdout.write(${JSON.stringify(VALID_PROBE_OUTPUT)})`],
});
try {
const result = await probePromptText(2_000);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.layers["personality"]).toMatchObject({ text: null, reason: "unmapped", bytes: 0 });
expect(result.layers["tools"]).toMatchObject({ text: null, reason: "unmapped", bytes: 0 });
// The base prompt keeps its own contract: readable text stays "ok" and an
// unexpanded template stays "not-exposed" - never "unmapped".
expect(result.layers["base-instructions"]).toMatchObject({ text: "Base prompt body.", reason: "ok" });
}
} finally {
if (previousHome === undefined) delete process.env.CODEX_HOME;
else process.env.CODEX_HOME = previousHome;
}
});
});

describe("runtime resolution and failure classification", () => {
test("a runtime the shared resolver finds is spawned, not reported missing", async () => {
// Issue 4458: the old four-path POSIX check reported "codex binary not
Expand Down
Loading