fix: keep suppressed notification names readable (#73)
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
@@ -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` 摘要输出。
|
||||
|
||||
|
||||
@@ -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<string, number>): JsonRecord {
|
||||
return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))) as JsonRecord;
|
||||
function countRecordEntries(input: Record<string, number>, 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 {
|
||||
|
||||
@@ -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<string, number> {
|
||||
const output: Record<string, number> = {};
|
||||
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 : {};
|
||||
|
||||
Reference in New Issue
Block a user