fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies (#10038)

* fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies

Extracted from PR #9439 (agentic conversation tracking). Most of the
original scope this commit was cherry-picked from (CHAT_LOG_MAX_BODY_KB
env var support, the estimateSizeFast() earlyExitAt parameterization)
turned out to already be present on the current upstream/release/v3.8.50
tip -- confirmed via diff and by running check-env-doc-sync.test.ts /
tests/unit/chatcore-log-truncation.test.ts against pristine upstream
before making any changes here. Only two genuine gaps remained:

1. CHAT_LOG_MAX_BODY_KB was read by getChatLogMaxBodyBytes() but
   undocumented in .env.example and docs/reference/ENVIRONMENT.md --
   tests/unit/check-env-doc-sync.test.ts flags any env var read in code
   but missing from both doc files. Documented it (both required --
   the same test enforces the pairing).

2. truncateForLog()'s summary only computed messageCount from
   obj.messages (OpenAI-chat/Gemini field name) -- a large /v1/responses
   request (which uses input[], not messages[]) got summarized with no
   count at all, leaving the dashboard's "Full Conversation" panel
   nothing to base its "N messages not shown" placeholder on for any
   Responses-API conversation, even though the same summarization logic
   applies to it.

Test plan:
- TDD: tests/unit/chatcore-log-truncation.test.ts's new regression test
  ("captures a message count for Responses API bodies too") confirmed
  failing against the pre-fix code, passing after.
- tests/unit/check-env-doc-sync.test.ts confirms CHAT_LOG_MAX_BODY_KB no
  longer appears in codeMissingEnv (remaining drift in that test is
  pre-existing/unrelated -- ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS,
  COMMANDCODE_API_URL, OMNIROUTE_STRICT_SYSTEM_PROVIDERS,
  TLS_FINGERPRINT_PROVIDERS -- confirmed identical on a pristine
  upstream/release/v3.8.50 checkout, base-red inherited: #9985).
- tests/unit/chatcore-log-truncation.test.ts -- 19/19 passing.
- npx tsc --noEmit / npm run lint -- clean.

⚠️ base-red inherited: #9985

* docs(logging): consolidate CHAT_LOG_MAX_BODY_KB into a single entry per file

The variable was already documented (with a stale src/lib/chatLogTruncation.ts
reference in .env.example); keep the new richer entries next to the CHAT_LOG_*
family and drop the old duplicates.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Markus Hartung
2026-08-13 09:02:42 +02:00
committed by GitHub
parent 2f264d96dc
commit d925f6bf73
5 changed files with 47 additions and 15 deletions

View File

@@ -1449,6 +1449,11 @@ APP_LOG_TO_FILE=true
# CHAT_LOG_ARRAY_TAIL_ITEMS=128 # Number of array items retained from tail (default: 128)
# CHAT_LOG_MAX_DEPTH=6 # Max nesting depth before truncation (default: 6)
# CHAT_LOG_MAX_OBJECT_KEYS=80 # Max object keys retained (default: 80, 0 = no limit)
# CHAT_LOG_MAX_BODY_KB=1024 # Whole request/response body size before it's replaced by a bare
# {_truncated, messageCount, ...} summary instead of the full clone
# (default: 1024 KB / 1MB). Raise this if the dashboard's "Full
# Conversation" transcript panel shows a placeholder instead of the
# actual messages for long agentic conversations.
# Maximum rows in the proxy_logs SQLite table.
# Default: 100000
@@ -2626,10 +2631,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# Used by: src/app/api/jobs/[id]/run-now/route.ts. Default: 30000 (30 seconds)
# OMNIROUTE_RUNNOW_TIMEOUT_MS=30000
# Maximum request/response body size before chat-log summarization, in KiB.
# Used by: src/lib/chatLogTruncation.ts. Default: 1024
# CHAT_LOG_MAX_BODY_KB=1024
# Adobe Firefly browser renewal and durable session cache (enabled by default).
# Used by: open-sse/services/adobeFireflySession.ts.
# ADOBE_FIREFLY_BROWSER_REFRESH=1

View File

@@ -753,6 +753,7 @@ The logging system writes to both stdout and rotated log files. All configuratio
| `CHAT_LOG_ARRAY_TAIL_ITEMS` | `128` | Number of array items retained from the tail when truncating chat log payloads. |
| `CHAT_LOG_MAX_DEPTH` | `6` | Max nesting depth before chat log payloads are truncated. |
| `CHAT_LOG_MAX_OBJECT_KEYS` | `80` | Max object keys retained in chat log payloads (0 = unlimited). |
| `CHAT_LOG_MAX_BODY_KB` | `1024` | Whole request/response body size (KB) before it's replaced by a bare summary instead of the full clone. Raise this if long agentic conversations show a placeholder instead of the real messages in the dashboard. |
| `CHAT_DEBUG_FILE` | `false` | When true, `serializeArtifactForStorage` skips size-based truncation. Debug only. |
---
@@ -1446,7 +1447,6 @@ These settings were introduced after the previous environment-contract snapshot.
| `OMNIROUTE_CHAT_VIRTUAL_TTL_MS` | `60000` (60 s) | `src/shared/middleware/chatBodyAdmission.ts` | Per-connection virtual admission lanes (#9654): idle-lane eviction TTL. |
| `OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS` | `64` | `src/shared/middleware/chatBodyAdmission.ts` | Per-connection virtual admission lanes (#9654): max concurrent sessions (lanes). |
| `OMNIROUTE_RUNNOW_TIMEOUT_MS` | `30000` | `src/app/api/jobs/[id]/run-now/route.ts` | Bounds how long a run-now call waits for an in-flight job before starting the queued run. |
| `CHAT_LOG_MAX_BODY_KB` | `1024` | `src/lib/logEnv.ts` | Maximum request or response body size before log summarization, in KiB. |
| `ADOBE_FIREFLY_BROWSER_REFRESH` | enabled | `open-sse/services/adobeFireflySession.ts` | Keeps IMS and browser-risk state fresh through account-scoped Chrome CDP sessions; set `0` to disable. |
| `ADOBE_FIREFLY_SESSION_DISK` | enabled | `open-sse/services/adobeFireflySession.ts` | Persists repaired Adobe sessions under `DATA_DIR`; set `0` for memory-only state. |
| `ADOBE_FIREFLY_MIN_SUBMIT_GAP_MS` | `12000` | `open-sse/services/adobeFireflySession.ts` | Minimum spacing between Adobe Firefly generate submissions. |

View File

@@ -60,9 +60,9 @@ export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
/**
* Truncate a large object for logging. If its JSON representation exceeds
* the configured max body size (getChatLogMaxBodyBytes()), return a
* lightweight summary instead of the full clone. This prevents
* persistAttemptLogs from holding multi-MB references to translatedBody
* getChatLogMaxBodyBytes() (default 1MB; CHAT_LOG_MAX_BODY_KB env override),
* return a lightweight summary instead of the full clone. This prevents
* persistAttemptLogs from holding unbounded references to translatedBody
* across 17 call sites per request.
*
* When the summarized object carries a `tools` definition, re-attach it
@@ -77,6 +77,9 @@ export function truncateForLog(value: unknown): Record<string, unknown> | null |
if (value === null || value === undefined) return value as null | undefined;
if (typeof value !== "object") return value as unknown as Record<string, unknown>;
const maxBodyBytes = getChatLogMaxBodyBytes();
// Pass maxBodyBytes as the early-exit point — otherwise estimateSizeFast's
// own default 256KB early-exit caps what it can ever report, silently
// making any configured threshold above 256KB unreachable (#trunc-limit-config).
const estimatedSize = estimateSizeFast(value, maxBodyBytes);
if (estimatedSize <= maxBodyBytes) return value as Record<string, unknown>;
// Object is too large — return a summary instead of a deep clone
@@ -88,6 +91,11 @@ export function truncateForLog(value: unknown): Record<string, unknown> | null |
if (typeof obj.model === "string") summary.model = obj.model;
if (typeof obj.provider === "string") summary.provider = obj.provider;
if (Array.isArray(obj.messages)) summary.messageCount = obj.messages.length;
// Responses API bodies use `input[]`, not `messages[]` (OpenAI-chat/Gemini-only
// field name) — without this, a large /v1/responses request got summarized
// with no count at all, leaving the dashboard's "Full Conversation" panel
// nothing to base its "N messages not shown" placeholder on.
else if (Array.isArray(obj.input)) summary.messageCount = obj.input.length;
if (Array.isArray(obj.contents)) summary.contentCount = obj.contents.length;
if (typeof obj.stream === "boolean") summary.stream = obj.stream;
if (Array.isArray(obj.tools)) summary.tools = cloneBoundedChatLogPayload(obj.tools);

View File

@@ -179,10 +179,17 @@ export function getChatLogMaxObjectKeys(): number {
}
/**
* Was a hardcoded/default 8KB — trivially exceeded by any real multi-turn
* agentic conversation, meaning the dashboard's "Full Conversation" panel
* could only ever show a placeholder instead of the actual messages for
* nearly every logged row of any conversation with real substance.
* Cap for a single logged request/response body before it gets replaced by a
* bare {_truncated, _originalBytes, messageCount, ...} summary instead of the
* full clone (open-sse/handlers/chatCore/logTruncation.ts::truncateForLog()).
* Was a hardcoded 8KB — trivially exceeded by any real multi-turn agentic
* conversation, which meant the dashboard's "Full Conversation" transcript
* panel could only ever show a placeholder instead of the actual messages
* for nearly every logged row. Bumped 128x (to 1MB) by default and exposed
* as an operator override for anyone who needs it even larger (or smaller,
* on a memory-constrained box) — see the same "protect memory across many
* call sites per request" reasoning truncateForLog()'s own doc comment
* explains for why some cap must still exist.
*/
export function getChatLogMaxBodyBytes(): number {
return parsePositiveInt(process.env.CHAT_LOG_MAX_BODY_KB, 1024) * 1024;

View File

@@ -134,7 +134,7 @@ test("truncateForLog summarizes oversized payloads instead of cloning", () => {
provider: "openai",
stream: true,
// distinct object references so estimateSizeFast (WeakSet-dedup) counts each one
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(64) })),
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(500) })),
contents: [{ a: 1 }],
};
const summary = truncateForLog(huge) as Record<string, unknown>;
@@ -150,6 +150,22 @@ test("truncateForLog summarizes oversized payloads instead of cloning", () => {
assert.notEqual(summary, huge);
});
test("truncateForLog captures a message count for Responses API bodies too (input[], not messages[])", () => {
// Live bug: a large /v1/responses request got summarized with NO count at
// all (messages/contents are OpenAI-chat/Gemini-only field names), so the
// "Full Conversation" dashboard panel had nothing to base its "N messages
// not shown" placeholder on for any Responses-API conversation, even
// though the exact same 8KB summarization applies to it.
const huge = {
model: "gpt-5",
stream: true,
input: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(500) })),
};
const summary = truncateForLog(huge) as Record<string, unknown>;
assert.equal(summary._truncated, true);
assert.equal(summary.messageCount, 50000);
});
test("truncateForLog keeps a bounded `tools` field alive when the request is summarized", () => {
// A request whose message history alone blows well past the 8KB summary
// threshold, but which also carries `tools` — a field that used to be
@@ -184,7 +200,7 @@ test("truncateForLog keeps a bounded `tools` field alive when the request is sum
model: "gpt-4o",
provider: "openai",
stream: true,
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(64) })),
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(500) })),
tools,
};
@@ -216,7 +232,7 @@ test("truncateForLog bounds an oversized `tools` array to the configured tail-it
}));
const huge = {
model: "gpt-4o",
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(64) })),
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(500) })),
tools: manyTools,
};