From 0092f55249edcce71ea21237fc7fae046e6bc678 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:16:50 +0800 Subject: [PATCH] fix: keep suppressed notification names readable (#73) Co-authored-by: Codex --- docs/reference/spec-v01-backend-adapter.md | 2 +- src/backend/codex-stdio.ts | 10 +++++---- src/selftest/cases/30-codex-stdio.ts | 25 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/reference/spec-v01-backend-adapter.md b/docs/reference/spec-v01-backend-adapter.md index 958855a..253461b 100644 --- a/docs/reference/spec-v01-backend-adapter.md +++ b/docs/reference/spec-v01-backend-adapter.md @@ -69,7 +69,7 @@ Adapter 输出给 runner 的 event 类型至少包括: 事件必须有上限和分页友好形态。大型日志、完整 stdout 或完整 trace 应进入 logPath 或后续 artifact,不得一次性塞入单个 event 造成输出爆炸。 -Codex app-server 的低价值内部 notification 必须在 AgentRun adapter 层收敛,不得要求 HWLAB Web/CLI 或其他消费侧自行过滤。以下事件默认不作为 durable trace event 持久化:`item/reasoning/textDelta`、纯 `reasoning` item 的 `item/started|item/completed`、非 `commandExecution` item 的通用 `item/started|item/completed`、`thread/tokenUsage/updated`、`account/rateLimits/updated`、普通 `warning` 和 `configWarning`。adapter 可以输出一条有界 `backend_status.phase=codex-app-server-notifications-suppressed` 摘要,只包含计数、method 和 item type,不包含 reasoning 文本、Secret、token 或 env value。真实 `agentMessage`、`commandExecution`、`command_output`、error、terminal 和关键生命周期事件必须继续保留。 +Codex app-server 的低价值内部 notification 必须在 AgentRun adapter 层收敛,不得要求 HWLAB Web/CLI 或其他消费侧自行过滤。以下事件默认不作为 durable trace event 持久化:`item/reasoning/textDelta`、纯 `reasoning` item 的 `item/started|item/completed`、非 `commandExecution` item 的通用 `item/started|item/completed`、`thread/tokenUsage/updated`、`account/rateLimits/updated`、普通 `warning` 和 `configWarning`。adapter 可以输出一条有界 `backend_status.phase=codex-app-server-notifications-suppressed` 摘要,只包含总数、`methods: [{ method, count }]` 和 `itemTypes: [{ itemType, count }]`,不包含 reasoning 文本、Secret、token 或 env value。method 和 item type 不得作为 JSON object key 输出,避免 `thread/tokenUsage/updated` 这类协议名被 redaction 误判为敏感 key。真实 `agentMessage`、`commandExecution`、`command_output`、error、terminal 和关键生命周期事件必须继续保留。 `commandExecution` 的 `tool_call` event 只能输出面向人和消费侧的扁平字段,例如 `method`、`itemId`、`toolName`、`type`、`command`、`cwd`、`status`、`processId` 和 `valuesPrinted=false`。不得把 Codex app-server 的原始 `item` JSON、`itemPreview` 或嵌套协议摘要写入 `message`、`outputSummary`、`stdoutSummary` 或 payload;命令实际 stdout/stderr 只通过 `command_output` 或 completed `commandExecution` 摘要输出。 diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index 4227dd7..bd630c7 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -653,15 +653,17 @@ function suppressedNotificationEvents(summary: SuppressedNotificationSummary): B payload: { phase: "codex-app-server-notifications-suppressed", total: summary.total, - byMethod: sortCountRecord(summary.byMethod), - byItemType: sortCountRecord(summary.byItemType), + methods: countRecordEntries(summary.byMethod, "method"), + itemTypes: countRecordEntries(summary.byItemType, "itemType"), valuesPrinted: false, }, }]; } -function sortCountRecord(input: Record): JsonRecord { - return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))) as JsonRecord; +function countRecordEntries(input: Record, keyName: "method" | "itemType"): JsonRecord[] { + return Object.entries(input) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([name, count]) => ({ [keyName]: name, count }) as JsonRecord); } function isSuppressedCodexStatusNotification(method: string): boolean { diff --git a/src/selftest/cases/30-codex-stdio.ts b/src/selftest/cases/30-codex-stdio.ts index 0c632ae..bc46104 100644 --- a/src/selftest/cases/30-codex-stdio.ts +++ b/src/selftest/cases/30-codex-stdio.ts @@ -153,6 +153,20 @@ const selfTest: SelfTestCase = async (context) => { const suppression = noisyItems.find((event) => event.type === "backend_status" && eventPayload(event).phase === "codex-app-server-notifications-suppressed"); assert.ok(suppression, "suppression summary must be emitted when noisy notifications are filtered"); assert.equal(eventPayload(suppression ?? { payload: {} }).total, 8); + assert.deepEqual(countEntriesByName(eventPayload(suppression ?? { payload: {} }).methods, "method"), { + "account/rateLimits/updated": 1, + "configWarning": 1, + "item/completed": 1, + "item/reasoning/textDelta": 2, + "item/started": 1, + "thread/tokenUsage/updated": 1, + "warning": 1, + }); + assert.deepEqual(countEntriesByName(eventPayload(suppression ?? { payload: {} }).itemTypes, "itemType"), { + reasoning: 4, + }); + assert.equal(eventPayload(suppression ?? { payload: {} }).byMethod, undefined, "suppression summary must not use method names as JSON keys because redaction treats token-like key names as sensitive"); + assert.equal(JSON.stringify(suppression).includes("thread/tokenUsage/updated"), true, "suppressed tokenUsage method name should remain visible as metadata, not redacted as a key"); assert.equal(JSON.stringify(noisyEvents).includes("internal reasoning must not become durable trace text"), false, "suppression summary must not leak reasoning text"); assertNoSecretLeak(noisyEvents); @@ -204,6 +218,17 @@ function eventPayload(event: { payload: unknown }): JsonRecord { return typeof event.payload === "object" && event.payload !== null && !Array.isArray(event.payload) ? event.payload as JsonRecord : {}; } +function countEntriesByName(value: unknown, keyName: "method" | "itemType"): Record { + const output: Record = {}; + if (!Array.isArray(value)) return output; + for (const entry of value) { + if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue; + const record = entry as JsonRecord; + if (typeof record[keyName] === "string" && typeof record.count === "number") output[record[keyName]] = record.count; + } + return output; +} + function eventPayloadItem(event: { payload: unknown }): JsonRecord { const item = eventPayload(event).item; return typeof item === "object" && item !== null && !Array.isArray(item) ? item as JsonRecord : {};