docs/design/2026-07-14-chat-record-daemon-transcript-block-projection.md
DaemonTranscriptBlock projection with diagnostics and completeness informationImplementation result: record preparation, the ACP replay machine, pure live/replay builders, the CLI adapter, provenance-aware compaction, the SDK normalizer/reducer, and the opt-in SDK facade have all landed. The default daemon browser bundle remains within its 151 KiB budget; the minified transcript browser bundle is 67,730 bytes. The separate daemon and daemon/transcript artifacts total 222,335 bytes, while an artifact importing both measures 222,722 bytes. The additional 387 bytes are combined-module wrapper overhead, so callers should treat the transcript subpath as an explicit opt-in cost. Synchronous performance baselines and Web Worker guidance are documented in the SDK README.
Web callers use a separate opt-in SDK subpath:
import {
projectChatRecordsToDaemonTranscript,
type ChatRecordTranscriptProjection,
} from "@qwen-code/sdk/daemon/transcript";
const projection = projectChatRecordsToDaemonTranscript(records);
const { blocks, diagnostics, complete, truncated } = projection;
This synchronous function does not start the daemon, Express, or an ACP child process; access the file system, network, DOM, or browser storage; or parse JSONL text. It accepts raw append-only records after JSON.parse and internally performs:
runtime validation
-> active leaf selection
-> parentUuid chain reconstruction
-> same-UUID fragment aggregation
-> persisted transcript replay
-> SessionUpdate normalization
-> DaemonTranscriptBlock projection
The shared implementation is divided into three deep modules with explicit ownership:
packages/core/src/utils/transcript-records.ts
-> package export @qwen-code/qwen-code-core/transcriptRecords
-> browser-safe record preparation
-> active chain, aggregation, gaps, diagnostics
packages/acp-bridge/src/transcript-replay.ts
-> browser-safe replay machine
-> shared pure SessionUpdate builders
packages/sdk-typescript/src/daemon/ui/chat-record-transcript.ts
-> SDK adapter
-> normalizer/reducer/finalize
-> public projection interface
The CLI's HistoryReplayer and the live MessageEmitter, ToolCallEmitter, and PlanEmitter all reuse the pure update builders from acp-bridge. This prevents drift from merely moving from “CLI versus Web” to “live versus replay”: record interpretation and update construction each have a single implementation.
The SDK adapter wraps the same SessionUpdate values as ID-less DaemonEvent values, reuses the existing normalizeDaemonEvent and transcript reducer, and ultimately returns blocks, diagnostics, complete, and truncated.
The target scenario is read-only rendering in WebShell of persisted JSONL generated by qwen -p, for example:
/root/.qwen/projects/-root--qwen-workspace/chats/<session-id>.jsonl
The browser has already obtained the file contents through a host, file picker, or another trusted read path, and is responsible for parsing the JSONL text into unknown records. The complete path after that is:
parsed append-only records
-> shared record preparation
-> shared transcript replay
-> DaemonTranscriptBlock projection
-> WebShellTranscript
Callers do not need to understand the parentUuid tree, the active branch after rewind, append fragments with the same UUID, session artifact records, or history gaps. Leaving these persistence semantics to callers would create a shallow module: the interface would appear to be a single function, but using it correctly would require callers to reimplement SessionService knowledge.
This design does not use compactedReplay. That is the bounded in-memory recovery window maintained by the daemon for live sessions; this utility processes persisted records explicitly supplied by the caller. Offline projection has no block-count limit by default, but retains the safety limit for an individual text block and explicitly reports all lossy processing through diagnostics and truncated.
/load Replays JSONLThe current response-mode /load does not pass JSONL directly to the SDK. The full path is:
SessionService.loadSession
-> JSONL parse
-> last non-artifact leaf
-> buildOrderedUuidChain
-> same-UUID aggregateRecords
-> ResumedSessionData.conversation.messages
QwenAgent.loadSession
-> collectHistoryReplayUpdates
-> HistoryReplayer
-> MessageEmitter / ToolCallEmitter / PlanEmitter
-> SessionUpdate[] in LOAD_REPLAY_META_KEY
acp-bridge restoreSession
-> extractLoadReplayResponse
-> BridgeClient.seedSessionUpdates
-> prepareSessionUpdateFrames
-> EventBus.seedReplayEvents
-> compactedReplay + liveJournal
DaemonSessionClient.load
-> replaySnapshot
-> normalizeDaemonEvent
-> reduceDaemonTranscriptEvents
-> DaemonTranscriptState.blocks
The first half of stream-mode /load is still produced by HistoryReplayer; the updates enter the pending restore EventBus as ACP notifications instead of being included in the load response. Both modes ultimately pass through the same bridge frame preparation, normalizer, and reducer.
The current implementation has three branches that must converge:
SessionService and SessionTranscriptReader each have their own aggregateRecords implementation.SessionService chooses the last non-artifact record as the leaf, while SessionTranscriptReader currently chooses the last structurally valid record. Their semantics differ when an artifact happens to be at the end of the file.Config and the Node runtime.This design does not create a separate JSONL-to-blocks shortcut. Instead, it extracts the browser-safe record preparation and SessionUpdate construction from the path above, then continues to use the daemon's existing normalization/reduction tail.
SessionUpdate construction rules.Config.EventBus, SSE cursors, Last-Event-ID, or compactedReplay.Record preparation is owned by core's persisted-session model. Add a browser-safe leaf:
packages/core/src/utils/transcript-records.ts
-> @qwen-code/qwen-code-core/transcriptRecords
This module:
leafUuid, or defaults to the last valid non-artifact conversation record;parentUuid;HistoryGap;SessionService;parentUuid values, corrupted records, and skipped artifact records; andComplete arrays and streaming indexes are read differently, so they share the same semantic primitives rather than forcing SessionTranscriptReader to load the entire file into memory:
validateTranscriptRecord
isTranscriptConversationRecord
selectTranscriptLeaf
walkTranscriptUuidChain(lookup)
aggregateTranscriptRecordFragments
prepareTranscriptRecords composes these primitives for raw arrays. SessionService uses the composed function directly. SessionTranscriptReader retains its byte-offset index and paginated reads, but uses the same classifier, lookup-based chain walker, and aggregator. The existing buildOrderedUuidChain is folded into this implementation and must not remain as a second walk.
This both removes the two aggregateRecords implementations and fixes the reader's semantic discrepancy when an artifact is the last record, without sacrificing its streaming index or paginated reads.
This leaf may import only browser-safe types and pure functions. It must not import fs, path, Buffer, the ChatRecordingService class, or provider runtime code.
Core currently has no exports map. The implementation must explicitly preserve exports for the root, transcriptRecords, package.json, and the existing ./dist/* deep imports. Adding a browser leaf must not accidentally close the @qwen-code/qwen-code-core/dist/... paths recorded as compatible by the repository.
SessionUpdate semantics belong to ACP, so the replay machine and pure update builders live in:
packages/acp-bridge/src/transcript-replay.ts
-> @qwen-code/acp-bridge/transcriptReplay
This module hides:
Deleting this module would redistribute complexity across CLI replay, live emitters, and SDK projection, so it passes the deletion test and has sufficient depth.
The replay machine does not duplicate the existing update-construction rules in MessageEmitter, ToolCallEmitter, and PlanEmitter. The acp-bridge leaf provides pure builders used only by adapters, such as:
createUserMessageUpdate
createAgentMessageUpdate
createAgentThoughtUpdate
createUsageUpdate
createToolCallStartUpdate
createToolCallResultUpdate
createPlanUpdate
The builders accept only structured parameters and return SessionUpdate. They do not access Config, registries, i18n, or the network.
CLI live emitters:
runtime input
-> CLI metadata adapter
-> shared builder
-> sendUpdate
HistoryReplayer:
prepared ChatRecord
-> replay machine
-> shared builder
-> sendUpdate
SDK offline projection:
prepared ChatRecord
-> replay machine
-> shared builder
-> id-less DaemonEvent
-> normalizer/reducer
Diff previews, Todo extraction, tool-content transformation, usage-to-plan ordering, and provenance fallbacks must live in the shared builders or their private helpers. Live emitters retain only asynchronous sending and runtime enrichment.
The SDK facade lives in a separate opt-in entry:
packages/sdk-typescript/src/daemon/ui/chat-record-transcript.ts
packages/sdk-typescript/src/daemon/transcript.ts
@qwen-code/sdk/daemon/transcript
It reuses the daemon UI normalizer and reducer but does not enter the default @qwen-code/sdk/daemon browser bundle. Callers only need to install the SDK and do not depend directly on core or acp-bridge subpaths.
Add two internal leaf exports:
@qwen-code/qwen-code-core/transcriptRecords
@qwen-code/acp-bridge/transcriptReplay
Constraints:
process, Buffer, the DOM, or storage..d.ts must inline public input/projection types and must not reference an acp-bridge subpath that exists only as a dev dependency.The public SDK facade accepts readonly unknown[]. After internal validation, the core leaf produces:
export interface TranscriptRecordInput {
readonly uuid: string;
readonly parentUuid: string | null;
readonly sessionId: string;
readonly timestamp?: string;
readonly type: "user" | "assistant" | "tool_result" | "system";
readonly subtype?: string;
readonly message?: {
readonly role?: string;
readonly parts?: readonly unknown[];
};
readonly usageMetadata?: unknown;
readonly toolCallResult?: unknown;
readonly systemPayload?: unknown;
}
export interface TranscriptReplayGapInput {
readonly childUuid: string;
readonly missingParentUuid: string;
}
export interface PreparedTranscriptRecords {
readonly sessionId?: string;
readonly records: readonly TranscriptRecordInput[];
readonly gaps: readonly TranscriptReplayGapInput[];
readonly diagnostics: readonly TranscriptProjectionDiagnostic[];
}
Fatal caller errors throw TranscriptProjectionInputError directly and return no partial result:
export type TranscriptProjectionInputErrorCode =
| "invalid_records"
| "invalid_max_blocks"
| "leaf_not_found"
| "mixed_session_ids";
export class TranscriptProjectionInputError extends TypeError {
readonly code: TranscriptProjectionInputErrorCode;
}
records is not an array.options.maxBlocks is not a positive safe integer.leafUuid does not exist.sessionId values are mixed in one projection.The SDK entry exports this error consistently. Core's internal validation error is mapped at the facade boundary so an internal package class does not leak into the public .d.ts. Other than these cases, a malformed individual record must not make the entire projection throw.
When an individual record or nested payload is malformed, preserve recoverable history wherever possible and emit a diagnostic:
parentUuid values, and unknown record types.serverTimestamp for those records.parentUuid values among fragments with a duplicate UUID, keep the first fragment and report the conflict.parentUuid is missing.parentUuid values form a cycle.chat_compression, ui_telemetry, file_history_snapshot, and artifact records, according to existing semantics without affecting complete.Empty input returns empty blocks with complete set to true. Artifact-only input likewise returns an empty transcript, with an informational diagnostic.
An explicit leafUuid must point to a conversation record. Matching only an artifact record is equivalent to the leaf not existing. Artifact records do not enter the UUID chain or participate in duplicate-parent conflict detection.
export interface TranscriptProjectionDiagnostic {
readonly code: string;
readonly severity: "info" | "warning" | "error";
readonly message: string;
readonly affectsCompleteness: boolean;
readonly recordIndex?: number;
readonly recordId?: string;
readonly path?: string;
}
Diagnostic messages must not contain unredacted arguments, results, tokens, or credentials. Callers should branch on code; message is only for logging and default presentation.
projection.complete means:
affectsCompleteness set to true;The first version stabilizes at least the following diagnostic codes. Codes are a compatibility contract; messages are not.
| code | affectsCompleteness | Meaning |
|---|---|---|
| invalid_record | true | An entire record was skipped |
| invalid_timestamp | false | Content was retained without historical time |
| conflicting_parent_uuid | true | Same-UUID fragments have conflicting parents |
| history_gap | true | The active chain is missing a parent |
| parent_cycle | true | The active chain contains a cycle |
| malformed_part | true | A recognized malformed part was skipped |
| unknown_record_or_part | true | An unknown extension may contain visible data |
| ambiguous_tool_call_correlation | true | A tool result cannot be correlated uniquely |
| presentation_fallback | false | Presentation adapter failed; fallback used |
| transcript_blocks_truncated | true | maxBlocks removed older blocks |
| transcript_text_truncated | true | A text block exceeded the character limit |
Artifact-only input may use an informational diagnostic without affecting complete. Adding a code later must not change the affectsCompleteness semantics of an existing code.
The shared layer emits complete SessionUpdate values and preserves projection provenance:
import type { SessionUpdate } from "@agentclientprotocol/sdk";
export interface TranscriptReplayEmission {
readonly sourceRecordId: string;
readonly sourceTimestamp?: string;
readonly emissionOrdinal: number;
readonly update: SessionUpdate;
}
An emission corresponds to one record projection, so the outer shape retains a singular sourceRecordId. When written to SessionUpdate, it becomes a single-element sourceRecordIds array for safe merging by subsequent compaction/upsert operations.
export interface TranscriptReplayUsageState {
readonly promptTokens: number;
readonly cachedTokens: number;
readonly candidateTokens: number;
readonly apiTimeMs: number;
}
export interface PendingTranscriptToolCall {
readonly callId: string;
readonly toolName: string;
readonly sourceRecordId: string;
readonly sourceTimestamp?: string;
}
export interface TranscriptReplayStateV1 {
readonly v: 1;
readonly pendingToolCalls: readonly PendingTranscriptToolCall[];
readonly cumulativeUsage: TranscriptReplayUsageState;
}
export interface TranscriptReplayMachineOptions {
readonly initialState?: TranscriptReplayStateV1;
readonly gaps?: readonly TranscriptReplayGapInput[];
readonly presentation?: TranscriptReplayPresentationAdapter;
readonly onDiagnostic?: (
diagnostic: TranscriptProjectionDiagnostic,
) => void;
}
Replay state must be versioned, and snapshot returns a detached copy. Malformed pending entries in initialState are filtered with a diagnostic; invalid or non-finite usage is reset to zero with a diagnostic. An unknown state version is rejected directly to avoid continuing pagination with incorrect state.
For compatibility with transcript cursors issued before deployment, legacy state without v is promoted directly to v1 when it strictly matches the current { pendingToolCalls, cumulativeUsage } shape. An explicit unknown v is still rejected. The legacy branch parses only this one published shape and does not evolve into a second state schema.
export interface TranscriptReplayMachine {
project(
record: TranscriptRecordInput,
): Iterable<TranscriptReplayEmission>;
finalize(): Iterable<TranscriptReplayEmission>;
snapshot(): TranscriptReplayStateV1;
}
export function createTranscriptReplayMachine(
options?: TranscriptReplayMachineOptions,
): TranscriptReplayMachine;
project returns a lazy iterator. The CLI immediately awaits sendUpdate after obtaining each emission and requests the next emission only after the send succeeds. State changes after a generator's yield therefore commit only after the previous send succeeds.
The interface must explicitly document these iteration constraints:
project.finalize is idempotent; its second call returns an empty iterator.finalize must catch send errors individually, continue attempting the remaining dangling cleanup, and retain the first cleanup error.AggregateError when both a replay error and a cleanup error exist.The SDK adapter has no external asynchronous send failure and can consume each iterator completely.
Call IDs follow this precedence:
functionCall.id, toolCallResult.callId, or functionResponse.id.ambiguous_tool_call_correlation diagnostic.finalize.Synthetic IDs use the qwen-replay-tool: prefix. The machine checks them for collisions with explicit IDs and earlier synthetic IDs, appending a stable occurrence suffix on collision.
A stable fallback guarantees only deterministic identity; it cannot guarantee correct correlation when information is absent.
Record identity must travel through the CLI, daemon, and SDK rather than existing only on the outer emission. A text block usually comes from one record, while a tool block absorbs both start and result records, so wire events and blocks use an ordered, deduplicated array. Replay builders add this to SessionUpdate._meta:
{
qwenTranscript: {
sourceRecordIds: ["..."]
},
timestamp: 1783958400000
}
Constraints:
sourceRecordIds are not EventBus IDs and must not be written to event.id or participate in Last-Event-ID.sourceTimestamp to a finite epoch value in milliseconds at the adapter seam and continue to reuse the existing timestamp field.[gap.childUuid] and the child record's timestamp.SessionUpdate values sent by CLI HistoryReplayer and the SDK offline adapter use identical metadata.qwenTranscript.sourceRecordIds from qwenTranscript, then removes the internal transport object from presentation metadata.sourceRecordIds to DaemonUiEventBase and DaemonTranscriptBlockBase.sourceRecordIds are equal and all other merge conditions are satisfied.toolCallId and union sourceRecordIds in event order. Plan and other upsert blocks use the same stable-union rule.sourceRecordIds, preventing merges across record boundaries.toolCallId, it must stably union qwenTranscript.sourceRecordIds; result metadata must not overwrite start provenance.sourceRecordIds using structured equality and Map, not an unescaped delimiter join that allows a malicious UUID to cause key collisions.qwenTranscript retain current compaction behavior.This preserves identical record segmentation for both daemon /load modes and offline projection, so conformance tests do not need a test-only activeRecordId context.
export interface TranscriptReplayPresentationAdapter {
resolveToolMetadata(
toolName: string,
args: Readonly<Record<string, unknown>>,
): TranscriptReplayToolMetadata;
formatHistoryGap(gap: TranscriptReplayGapInput): string;
}
Config/tool registry to resolve title, kind, and locations, and uses CLI i18n to format history gaps.other, locations are empty, and history gaps use fixed SDK copy.If the adapter throws, the replay machine uses a deterministic fallback and emits a diagnostic instead of allowing presentation enrichment to terminate the entire transcript.
Provenance, Todo/diff/content, usage, and call correlation do not belong at this seam and must be decided by the shared implementation.
HistoryReplayer retains its existing calling interface but is reduced to an asynchronous adapter:
prepared records
-> seed replay state
-> machine.project(record)
-> await sendUpdate(emission.update) in order
-> machine.finalize() when requested
-> copy machine.snapshot()
-> clear active replay context
The following behavior remains in the CLI:
Config/tool-registry enrichment;messageRewriter.interceptUpdate;sendUpdate failure handling;AggregateError; andLoad, paginated transcript, and export paths must use the same record preparation and replay machine so the same JSONL does not produce different SessionUpdate values through different entry points.
export interface ChatRecordTranscriptOptions {
readonly leafUuid?: string;
readonly maxBlocks?: number;
}
export interface ChatRecordTranscriptProjection {
readonly blocks: readonly DaemonTranscriptBlock[];
readonly diagnostics: readonly TranscriptProjectionDiagnostic[];
readonly complete: boolean;
readonly truncated: boolean;
}
export function projectChatRecordsToDaemonTranscript(
records: readonly unknown[],
options?: ChatRecordTranscriptOptions,
): ChatRecordTranscriptProjection;
When options.maxBlocks is omitted, offline projection does not trim the block count. An explicit value must be a positive safe integer. When trimming occurs:
truncated is true;complete is false;diagnostics includes transcript_blocks_truncated; andThe offline adapter explicitly passes Number.MAX_SAFE_INTEGER as the default. It does not change DEFAULT_MAX_BLOCKS for online createDaemonTranscriptState or put Infinity into reducer state.
The SDK adapter's event path is:
TranscriptReplayEmission
-> id-less DaemonEvent(type = session_update)
-> normalizeDaemonEvent
-> reduceDaemonTranscriptEvents
-> finalizeOfflineDaemonTranscriptState
-> ChatRecordTranscriptProjection
Events have no ID because they do not come from EventBus. sourceTimestamp becomes serverTimestamp, and sourceRecordIds remain separate projection provenance.
The offline adapter uses a fixed reducer clock of 0, preventing Date.now from entering observable fields. The same input, options, and presentation adapter must produce a deeply equal projection; serverTimestamp represents the actual historical time.
The new private finalizeOfflineDaemonTranscriptState performs only offline-projection cleanup and is not exported from the default daemon entry:
streaming to false;An individual text block continues to use the SDK's safety character limit. When character truncation occurs, the reducer diagnostic hook must report transcript_text_truncated and set truncated=true and complete=false; it must not rely only on a visible [truncated] suffix.
To make block/text truncation observable, add an optional onTruncation(detail) to DaemonTranscriptReducerOptions. The detail includes at least the kind, block ID, and sourceRecordIds when present. Ordinary stores do not pass this callback; the offline adapter collects and deduplicates details into projection diagnostics. Do not infer truncation by scanning for [truncated], because user text may contain the same suffix.
UUIDs, call IDs, and parent IDs in offline input are untrusted strings. Before integration, change these transcript-reducer indexes to Map or null-prototype objects:
blockIndexById;toolBlockByCallId;permissionBlockByRequestId;activeAssistantBlockByParent;activeThoughtBlockByParent; andTests must cover __proto__, constructor, prototype, toString, and overlong IDs to ensure that they cannot break lookup, parent-child relationships, or trimming cleanup.
The tool-result builder may continue putting persisted artifacts in SessionUpdate metadata for use by the daemon bridge's artifact side channel. DaemonTranscriptBlock has no artifact field, however, and SDK offline projection does not return an artifact store.
Conformance is therefore divided into two layers:
SessionUpdate conformance includes artifacts.DaemonTranscriptBlock conformance explicitly ignores the artifact side channel.If WebShellTranscript needs artifact cards in the future, add a separate artifact projection rather than smuggling artifacts into transcript blocks.
CLI replay and SDK offline projection share the machine, so the following must match:
sourceRecordIds, and replay diagnostics.Live emitters and the replay machine share update builders, so fields in SessionUpdate generated for the same semantic event must match.
Config/tool registry.If the product requires field-for-field identical tool metadata, replay metadata must be persisted when the tool call is recorded and follow “persisted value first, deterministic fallback.” Historical truth must not be recomputed from the current registry.
Tests have six layers:
SessionUpdate values.sourceRecordIds during text/tool compaction.AggregateError.sourceRecordIds, normalization, record segmentation, truncation, malicious identifiers, and deterministic blocks.Cross-package paths:
raw records
-> SDK projectChatRecordsToDaemonTranscript
-> sdkProjection
raw records
-> shared record preparation
-> CLI HistoryReplayer
-> captured SessionUpdate with qwenTranscript metadata
-> SDK normalizer/reducer/finalize
-> cliProjection
Perform deep equality on the canonical projection. The canonicalizer may ignore only explicitly allowed adapter differences; it must not remove sourceRecordIds, timestamps, tool status, diagnostics, or truncation.
Also add daemon integration fixtures that verify response-mode and stream-mode /load retained replay match offline projection when no window truncation occurs. Tests must cross a subsequent turn boundary to cover bridge/compaction retention of qwenTranscript metadata and timestamps.
import { useMemo } from "react";
import {
projectChatRecordsToDaemonTranscript,
} from "@qwen-code/sdk/daemon/transcript";
import { WebShellTranscript } from "@qwen-code/web-shell";
function ReadonlyHistory({ records }: { records: readonly unknown[] }) {
const projection = useMemo(
() => projectChatRecordsToDaemonTranscript(records),
[records],
);
return (
<>
{projection.complete ? null : (
<TranscriptDiagnostics diagnostics={projection.diagnostics} />
)}
<WebShellTranscript blocks={projection.blocks} />
</>
);
}
The SDK owns data preparation and projection; WebShell owns only read-only rendering. WebShellTranscript does not add a records prop or start a provider, session, or network connection.
The public facade is a synchronous O(records + parts) projection and scans all input even if an explicit maxBlocks ultimately retains only the tail blocks. maxBlocks limits output memory, not computation.
Before implementation, establish time and peak-memory baselines using small, medium, and large real fixtures, and document the recommended main-thread limit in the SDK documentation. Hosts above that limit should invoke the same browser-safe interface in a Web Worker and pass the projection to the main thread.
The first version does not add a separate async/worker wrapper. Reconsider that adapter after a second real caller appears, avoiding a false seam with only one adapter.
The converter does not enter the default @qwen-code/sdk/daemon bundle. Add this package export:
"./daemon/transcript": {
"types": "./dist/daemon/transcript.d.ts",
"import": "./dist/daemon/transcript.js",
"require": "./dist/daemon/transcript.cjs"
}
Build requirements:
.d.ts files do not leak core/acp-bridge dev dependencies.daemon and daemon/transcript.The default daemon budget of 151 KiB does not increase for this feature.
SessionService and SessionTranscriptReader share classification, leaf selection, chain walking, and aggregation.SessionUpdate builders to acp-bridge and gradually migrate live emitters to them.HistoryReplayer into a CLI adapter while preserving its existing calling interface and error semantics.qwenTranscript metadata and extend bridge, compaction, normalizer, and reducer handling of sourceRecordIds.daemon/transcript facade and separate publish artifacts to the SDK.projection.blocks and show diagnostics.At each step, migrate existing consumers before deleting the old implementation so that no stage has two active-chain, aggregation, or update-builder rule sets at once.
HistoryReplayer adapter: approximately 60–100 lines.This is a cross-package core change. A maintainer must confirm the scope under the repository's core triage gate before implementation. Duplicate aggregation or update builders should not be retained merely to reduce the line count.
The projection can recover only information present in records. The following is explicitly unrecoverable or potentially lossy:
Config/registry/locale;parentToolCallId;maxBlocks; andEvery case that affects completeness must emit a diagnostic and set complete=false. Every actual trim must also set truncated=true.