docs/design/2026-08-13-privacy-safe-tool-result-boundary-diagnostics.md
Add opt-in debug-log events that explain where an oversized tool-result representation changes between production, model finalization, session recording, ACP or Headless projection, and the actual writer. The events contain only sizes, process-local HMACs, mutation state, and privacy-safe artifact state/kinds. Diagnostic failures remain isolated from tool execution and transport behavior.
The implementation covers every built-in tool-result route:
CoreToolScheduler records raw producer input and its terminal output for interactive, Headless, and agent executions, so scheduler-side persistence, truncation, hooks, and display compaction are attributable before finalization.execute() settles.finalizeToolResponses() covers interactive, Headless, ACP, agent, and speculative model-facing aggregation.ChatRecordingService.recordToolResult() is the shared recorder boundary.Custom adapters and prebuilt custom tool_result messages remain outside the built-in Headless route. Generic frame limits, backpressure, replay aggregate limits, and artifact lifecycle remain tracked separately.
Diagnostics run only when QWEN_DEBUG_LOG_FILE is enabled and a debug-log session is active. An event is eligible only when at least one textual representation exceeds 65,536 JSON UTF-8 bytes or the observed boundary changed a representation.
Each event records:
undecided, none, or reusable) and deduplicated kind enums only;The HMAC key is generated randomly once per process. Every string is hashed independently with an eight-byte byte-length prefix followed by its UTF-16LE code units; values are never concatenated before hashing. Hashing code units preserves distinctions between valid Unicode and lone-surrogate JavaScript strings while keeping equal values comparable inside one process without creating stable cross-process content fingerprints.
No event contains output text, prompts, artifact paths, artifact titles or URLs, session IDs, prompt IDs, tool-call IDs, tool names, arguments, or filesystem paths. Structured rich displays are not recursively inspected: the Phase 2 byte contract applies only to the textual model, display, ACP content/raw, and Headless content representations. Artifact summaries use the existing kind enum plus unknown; reusable persistence files contribute the safe file kind. Batch writer events keep summaries in the same order as their tool-call identifiers instead of collapsing mixed states or kinds.
The observer performs its enablement check before scanning or hashing values. All observation, hashing, classification, and logging code is wrapped in a failure boundary; exceptions are swallowed and never alter the value or write path.
A process-wide limiter emits at most 50 eligible events per 60-second window. Additional eligible events increment a suppressed counter. The first eligible event in a later window reports the accumulated count, then resets it.
The existing qwen serve large-pipe-frame observer remains the only daemon attribution mechanism for frames at or above 256 KiB. These diagnostics correlate representations and exact writer sizes but do not emit production telemetry or replace large-frame attribution.
A small Core utility owns event eligibility, exact JSON-string byte accounting, HMAC generation, artifact-state classification, rate limiting, and debug-log output. It accepts textual values lazily so disabled diagnostics do not traverse tool results.
Core call sites add observations at scheduler producer input/output, the speculative producer route, finalizer input/output, and recorder input/output. ACP adds its direct producer observation. Recorder-only diagnostic metadata is stripped before the transcript record is constructed.
A CLI-internal helper owns ACP and Headless projection correlation. It records projection input/output and associates eligible projected objects plus their safe artifact summaries with the later writer through weak references. Subagent progress carries only this closed-enum summary, only while diagnostics are enabled; raw persistence paths and structured artifacts never enter that event path. Eligibility includes both changed projections and oversized unchanged exemptions such as A2UI. This avoids marker parsing and avoids any schema or wire metadata change.
The change is diagnostic-only when disabled and does not modify tool results, projections, transcripts, schemas, ACP messages, Headless messages, SDK types, or protocol versions. Debug log files gain new JSON-shaped lines only when explicitly enabled. HMACs intentionally change after every process restart.
Focused tests cover exact JSON byte accounting (including escapes and Unicode), HMAC equality and mutation mismatch, identifier redaction, artifact tri-state/kinds, mixed batch artifact summaries, enablement, rate limiting, suppressed counts, failure isolation, Core boundary integration, ACP live/replay projection, ACP NDJSON byte counts, Headless JSON/stream-json writer byte counts, and Text retention without a tool-result wire event.
A deterministic fake-MCP exercise records before/after evidence for a 499,999-byte result across Headless JSON, stream-json, persistent stream-json/SDK transport, Text, and ACP where feasible. It verifies exact logged writer bytes, process-local HMAC correlation, absence of fixture text and identifiers in the log, unchanged producer artifact size/hash, and unchanged user-visible output.