Skip to content

🧩 feat: Map Message Content Parts to a UI Parts Union - #16374

Open
berry-13 wants to merge 10 commits into
canaryfrom
berry-13/chat-parts
Open

berry-13 wants to merge 10 commits into
canaryfrom
berry-13/chat-parts

Conversation

@berry-13

@berry-13 berry-13 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

LibreChat message content is its own vocabulary (TMessageContentParts, discriminated on ContentTypes), so anything that wants to treat a message the way the AI SDK does has to translate every part type by hand. This adds that translation once, in packages/data-provider, with no consumer changes.

toUIPart/toUIParts map content to a UI parts union modelled on AI SDK UIMessage.parts (matched against ai@7.0.114): text, reasoning, tool-<name> with an input-streaming | input-available | output-available | output-error state, file, source-url, step-start, and data-* for agent updates, summaries, activity labels, steers and errors. Parts stay one-to-one with content slots, so the position-equals-step-index invariant holds; a hole maps to step-start and back. fromUIPart/fromUIParts restore the stored content losslessly for everything except the stream-only text_delta. toUIMessage/fromUIMessage give a UIMessage view of TMessage, with agentIds, groupIds, activity labels, steers and summaries in metadata.

Type of change

  • Feature

Testing

Tested environments/configuration:

Unit level only; nothing consumes the mapping yet.

Automated tests:

  • Added packages/data-provider/src/parts.spec.ts: every ContentTypes member (a Record<ContentTypes, ...> makes a missing one a type error), round-trips for every lossless type, content arrays recorded by running the stream reducers from client/src/hooks/SSE/__tests__/steps.spec.ts (tool completed, streaming and cancelled, out-of-order holes, reasoning, handoff, failed summary, image), 250 generated sparse arrays checking order, holes and round-trip, and the reverse message mapping (empty content with text, edited text, attachment changes, metadata without a stored base)
  • cd packages/data-provider && npx jest: 52 suites, 2197 passed, 1 skipped
  • cd packages/data-provider && npx tsc --noEmit: clean
  • npm run static-checks -- --against origin/canary: all affected checks passed

Screenshots / recordings

No user-facing change.

Risk / compatibility

None. New exports only; no existing type or function changes.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T22:31:06.066896Z a496143 New commits
🔒 Security Review ✅ Completed 2026-09-25T18:39:51.314374Z 9d56547 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Lighthouse CI failed. The last 80 log lines contain the measured budgets and assertion failures.

│ 21      │ 'http://localhost:3080/api/convos?pinned=true&limit=100'                                                        │ 2811.822999999975  │ 3597.4869999999646 │ 200    │
│ 22      │ 'http://localhost:3080/api/mcp/servers'                                                                         │ 3192.341999999975  │ 4467.418999999965  │ 200    │
│ 23      │ 'http://localhost:3080/api/permissions/mcpServer/effective/all'                                                 │ 3193.828999999969  │ 3959.3769999999786 │ 200    │
│ 24      │ 'http://localhost:3080/api/prompts/groups?limit=10'                                                             │ 3194.1359999999695 │ 4467.147999999957  │ 200    │
│ 25      │ 'http://localhost:3080/api/keys?name=openAI'                                                                    │ 3489.8119999999763 │ 4077.521999999968  │ 200    │
│ 26      │ 'http://localhost:3080/api/presets'                                                                             │ 3490.612999999983  │ 4105.609999999957  │ 200    │
│ 27      │ 'http://localhost:3080/api/tags'                                                                                │ 3490.8489999999874 │ 4108.828999999969  │ 200    │
│ 28      │ 'http://localhost:3080/api/share/link/16390000-0000-4000-8000-000000000001'                                     │ 3491.0869999999704 │ 4467.68299999999   │ 200    │
│ 29      │ 'http://localhost:3080/api/messages/16390000-0000-4000-8000-000000000001'                                       │ 3492.765999999974  │ 4588.27999999997   │ 200    │
│ 30      │ 'http://localhost:3080/api/files/config'                                                                        │ 3493.4289999999746 │ 4364.290999999968  │ 200    │
│ 31      │ 'http://localhost:3080/api/agents/tools/web_search/auth'                                                        │ 3493.689999999973  │ 7136.970999999961  │ 200    │
│ 32      │ 'http://localhost:3080/api/endpoints/token-config'                                                              │ 3494.118999999977  │ 4624.563999999984  │ 200    │
│ 33      │ 'http://localhost:3080/api/agents/tools/calls?conversationId=16390000-0000-4000-8000-000000000001'              │ 3494.3649999999616 │ 4981.38499999998   │ 200    │
│ 34      │ 'http://localhost:3080/api/agents/chat/status/16390000-0000-4000-8000-000000000001?generationProtocolVersion=2' │ 4738.256999999954  │ 4997.268999999971  │ 200    │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┴────────────────────┴────────┘

Inspect .lighthouse HTML/JSON and e2e/lighthouse/README.md. Reuse loaded user/config data; overlap independent reads without bypassing authorization.

┌─────────┬────────────────────────────┬─────────────────────┬───────┐
│ (index) │ audit                      │ median              │ limit │
├─────────┼────────────────────────────┼─────────────────────┼───────┤
│ 0       │ 'largest-contentful-paint' │ 4761.192            │ 4500  │
│ 1       │ 'cumulative-layout-shift'  │ 0.01845228234538715 │ 0.1   │
│ 2       │ 'total-blocking-time'      │ 394.35699999999997  │ 500   │
└─────────┴────────────────────────────┴─────────────────────┴───────┘

  1) [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

    Error: Median largest-contentful-paint must stay within 4500

    expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 4500
    Received:    4761.192

       at audit.ts:159

      157 |   console.table(measured);
      158 |   for (const { audit, median, limit } of measured) {
    > 159 |     expect(median, `Median ${audit} must stay within ${limit}`).toBeLessThanOrEqual(limit);
          |                                                                 ^
      160 |   }
      161 |   return results;
      162 | }
        at auditPage (/home/runner/work/LibreChat/LibreChat/e2e/lighthouse/audit.ts:159:65)
        at /home/runner/work/LibreChat/LibreChat/e2e/lighthouse/load.spec.ts:33:19

    attachment #1: screenshot (image/png) ──────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/test-failed-1.png
    ────────────────────────────────────────────────────────────────────────────────────────────────

    Error Context: e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/error-context.md

    attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip
    Usage:

        npx playwright show-trace e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip

    ────────────────────────────────────────────────────────────────────────────────────────────────


🤖: global teardown has been started
2026-09-25 18:41:04 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-25 18:41:04 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ✅  Found user in Database
🤖:  ✅  Deleted 1 convos & 2 messages
🤖:  ✅  Deleted user from Database
🤖: global teardown has been started
2026-09-25 18:41:05 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-25 18:41:05 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ⚠️  User not found in Database
  1 failed
    [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

Open the full run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d56547e41

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated
Comment thread packages/data-provider/src/parts.ts Outdated
const sources = [...(results.organic ?? []), ...(results.topStories ?? [])];
return sources.map((source, i) => ({
type: 'source-url',
sourceId: `${attachment.toolCallId}-${i}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope source IDs to the attachment occurrence

When one message contains search attachments that reuse a provider toolCallId, this resets i for each attachment and produces duplicate sourceId values such as call_0-0 for different URLs. This is a supported case: client/src/hooks/SSE/useAttachmentHandler.ts:13-29 deliberately distinguishes snapshots by agent, turn, and host step, and the integration test at ContentParts.integration.test.tsx:366-398 covers repeated call IDs. Include those ownership fields or a message-wide ordinal so distinct sources retain distinct UI identities.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. Source ids carry a message-wide ordinal across every search attachment, and URLs are deduplicated message-wide. Covered by 'gives distinct sources distinct ids when tool call ids repeat'.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +407 to +409
case ContentTypes.IMAGE_FILE: {
const { source: _source, ...rest } = metadata;
return { type: ContentTypes.IMAGE_FILE, ...rest };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Write image-file UI edits back to content

When a mapped image_file part is edited through its UI representation, this branch ignores part entirely and restores the original image_file object from metadata. Changes to url, filename, or mediaType therefore disappear on fromUIPart/fromUIMessage, while the adjacent image-URL, video, and audio branches do consume the edited UI fields. Retain the stored image metadata but overwrite its filepath, filename, and type from the UI part.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. The image_file branch now applies the view's url, filename and mediaType over the stored record when they differ, so edits land and untouched parts still round-trip. Covered by 'writes image file edits back while keeping the stored record'.

Comment thread packages/data-provider/src/parts.ts Outdated
default:
return {
name: toolCall.name,
id: toolCall.id ?? toolCall.stepId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back from empty tool-call IDs

When a tool step is announced before the provider assigns an ID, applyToolCallsStep deliberately stores id: '' together with the stable host stepId, but nullish coalescing selects the empty string here instead of that step ID. toToolPart then emits an empty toolCallId because its later fallback has the same problem, so multiple in-flight calls can share the same UI identity and deltas or outputs can be associated with the wrong call. Treat an empty ID as missing and fall back to stepId or the synthetic index ID.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. An empty tool call id is treated as missing: agents calls fall back to stepId, then to the synthetic tool_call-. Covered by 'falls back from an empty tool call id to the host step id' and the persisted tool scenario, which asserts non-empty, distinct toolCallIds.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +609 to +612
const text = message.parts.reduce(
(joined, part) => (part.type === 'text' ? joined + part.text : joined),
'',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consolidate reverse mapping into one parts pass

fromUIMessage traverses message.parts here to collect text, traverses it again through fromUIParts, and, without a base message, performs separate filter and map passes for files. Thus every base-less conversion makes four O(n) passes over the same potentially large message-parts array even though text, content, and attachment files can be accumulated together; consolidate this work into the reconstruction loop. CLAUDE.mdL165-L168

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. fromUIMessage collects text, content and attachment files in one pass over the parts.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +275 to +282
const { runStepStatus, progress } = toolCall;
const failed = runStepStatus === 'failed' || runStepStatus === 'cancelled';
const hasOutput = output != null && output !== '';

let state: UIToolState = complete ? 'input-available' : 'input-streaming';
if (failed) {
state = 'output-error';
} else if (hasOutput || runStepStatus === 'completed' || (progress ?? 0) >= 1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor persisted tool failure markers

Persisted tool calls can signal terminal failure without a failed runStepStatus: background cancellation is stored as backgroundTask.cancelled, and rejected arguments are stored as inputValidationError. The existing renderer explicitly consumes both markers, but this calculation ignores them and can classify a cancelled or validation-failed call with output/progress as output-available. Include these durable markers when deriving the UI state so restored cancellation and failure are not shown as success.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. getToolFailure reads backgroundTask.cancelled and inputValidationError alongside failed/cancelled run steps, so those calls map to output-error. Covered by 'reports durable failure markers as output-error'.

Comment on lines +279 to +283
let state: UIToolState = complete ? 'input-available' : 'input-streaming';
if (failed) {
state = 'output-error';
} else if (hasOutput || runStepStatus === 'completed' || (progress ?? 0) >= 1) {
state = 'output-available';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Represent pending tool approvals in the UI state

When a tool call is paused for human approval, its stored approval object has complete JSON arguments and no output, so this initializes the part as ordinary input-available. The exported part has no approval field or approval lifecycle state, leaving consumers unable to distinguish a blocked call from one ready to execute even though Part.tsx:433-447 renders mandatory approval controls for this exact condition. Surface the approval payload and a distinct pending-approval state in the UI union.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. A stored approval without submitted output maps to the AI SDK approval-requested state with approval { id, requestReason }, and maps back to the stored call. Covered by 'reports a call paused for review as approval-requested'.

Comment thread packages/data-provider/src/parts.ts Outdated
if (!results) {
return [];
}
const sources = [...(results.organic ?? []), ...(results.topStories ?? [])];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include URL references when mapping search sources

A persisted web-search result may place cited links in references rather than organic or topStories; the existing sources renderer handles non-image/non-file reference links as ordinary sources in client/src/components/Web/Sources.tsx:597-636. This selection silently omits those URLs, so a message whose citations are reference-backed produces no corresponding source-url parts. Include link references when building the source list, with appropriate deduplication.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. Link-type references are included as sources, deduplicated by URL across the message; image and file references stay out. Covered by 'gives distinct sources distinct ids when tool call ids repeat'.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a815932762

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +690 to +691
conversationId: metadata?.conversationId ?? base?.conversationId ?? null,
parentMessageId: metadata?.parentMessageId ?? base?.parentMessageId ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit null parent IDs

When a UI edit promotes a message to a root by setting metadata.parentMessageId to null, nullish coalescing restores the old non-null ID from base instead. Since null is a valid TMessage.parentMessageId, distinguish an absent metadata value from an explicit null so reparenting does not silently leave the message under the wrong node. The same issue affects clearing conversationId.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. Identity fields now read metadata when the key is defined, so an explicit null parentMessageId or conversationId wins over the stored value (pickIdentity). Covered by 'keeps an explicit null parent instead of the stored one'.

Comment on lines +435 to +439
const output = typeof part.output === 'string' ? part.output : part.errorText;
return {
type: ContentTypes.TOOL_CALL,
tool_call: {
type: ToolCallTypes.TOOL_CALL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist errors from hand-built tool parts

When fromUIPart receives a hand-built output-error tool part without provider metadata, this stores errorText only as a normal output and records no failure status. Mapping that content back through toUIPart therefore reports output-available, turning a restored failure into a success; preserve a durable failed marker when constructing this tool call.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. A hand-built output-error tool part now stores runStepStatus: 'failed' on the rebuilt call, so it maps back as output-error with its errorText. Covered by 'persists the failure of a hand-built error tool part'.

Comment thread packages/data-provider/src/parts.ts Outdated
if (part == null) {
continue;
}
pushUnique(agentIds, part.agentId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include nested tool owners in message metadata

For persisted tool parts whose ownership exists only at tool_call.agentId, this reads only the outer part and omits that agent from metadata.agentIds. That nested fallback is an established storage shape—the ownership logic in packages/data-schemas/src/methods/message.ts explicitly resolves part.agentId ?? part.tool_call?.agentId—so restored multi-agent messages expose incomplete producer metadata unless the inner ID is included.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. Metadata agentIds falls back to tool_call.agentId when the outer part has none, matching the data-schemas ownership resolution. Covered by 'collects agents that own only a nested tool call'.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +665 to +670
const files = attachments.map(
(part) =>
byPath.get(part.url) ?? {
filepath: part.url,
...(part.filename && { filename: part.filename }),
type: part.mediaType,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply attachment edits to matched stored files

When an attachment URL matches a stored file, this returns the base object wholesale, so UI edits to that part's filename or mediaType are silently discarded. Merge the editable UI fields into the matched record while retaining non-UI fields such as file_id; otherwise only newly added attachments can have those fields changed.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. A matched stored file now takes the view's url, filename and mediaType where they differ from the derived values (applyFileEdits), keeping file_id and the rest. Covered by 'writes attachment renames onto the matched stored file'.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +277 to +282
const hasOutput = output != null && output !== '';

let state: UIToolState = complete ? 'input-available' : 'input-streaming';
if (failed) {
state = 'output-error';
} else if (hasOutput || runStepStatus === 'completed' || (progress ?? 0) >= 1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty legacy function output as submitted

For a legacy FunctionToolCall, the contract uses null to mean the output was not submitted, so '' is a valid completed empty result. Excluding the empty string here classifies such a call as input-available and omits its output from the UI part, presenting a completed call as still awaiting execution; use the call variant's null sentinel rather than a blanket non-empty check.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. Output submission is now decided by each variant's own sentinel: a legacy function call counts as submitted when output is not null, so '' is a completed empty result. Covered by 'treats an empty legacy function output as submitted'.

...base,
...pickMessageFields(metadata),
messageId: message.id,
isCreatedByUser: message.role === 'user',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the declared system message role

A valid UIMessage with role: 'system' is collapsed to the same TMessage shape as an assistant here, and a subsequent toUIMessage returns it as assistant. This is observable for LibreChat's existing system-generated messages, which use sender: 'System' in the assistant and agent error paths; encode that distinction in the reverse mapping and recognize it on the forward mapping, or remove system from the exported role union.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. UIMessage.role is narrowed to 'user' | 'assistant': a TMessage has only isCreatedByUser, so a system role could not survive the reverse mapping, and the type now says so.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +633 to +635
const resolveText = (text: string, contentless: boolean, base?: TMessage) => {
if (contentless || base?.text === undefined) {
return text;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry non-derived message text without a base

When no base is supplied, this always uses the concatenated UI text parts, even though the function's own invariant notes that a content-bearing TMessage.text is not necessarily derived from those parts. Thus fromUIMessage(toUIMessage(message)) loses a non-derived value such as the stored summary covered by the adjacent test whenever the message also has tool, reasoning, or differently worded text content; retain that original text in metadata when it differs so the documented base-less rebuild preserves it.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. When a content-bearing message's stored text differs from its joined text parts, toUIMessage carries it in metadata.text and a base-less fromUIMessage restores it. Covered by 'keeps stored text that the parts do not derive without a base'.

Comment on lines +219 to +220
if (text == null || typeof text === 'string') {
return { value: text ?? '' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve missing text fields during round-trips

For a valid placeholder such as { type: TEXT } or { type: THINK }, the optional payload is absent, but this normalizes it to ''; fromUIPart(toUIPart(part)) then adds text: '' or think: '' and violates the advertised lossless round-trip. Missing text updates are an actual stream shape (for example, edited content can carry only its index and type), so retain an absence marker instead of conflating it with an explicitly empty payload.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b71feb9. An absent text/think field is recorded as textAbsent and left off again on the way back, instead of becoming ''. Covered by 'keeps a text or think part without its text field'.

@berry-13
berry-13 added this pull request to stack #16380 September 25, 2026 19:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b71feb9bb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +298 to +300
case 'retrieval':
case 'file_search':
return { name: toolCall.type, id: toolCall.id || undefined, submitted: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat legacy retrieval outputs as submitted

For persisted retrieval or file_search calls that carry a top-level output, this branch hard-codes submitted: false and discards the result. That runtime shape is already supported by client/src/components/Chat/Messages/Content/Part.tsx:461-470 and outcome.ts:162-168; without a newer runStepStatus or progress marker, the restored call is therefore reported as input-streaming, and even with a completion marker its output is missing. Preserve the supported output and derive submission from it.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. retrieval and file_search calls read their top-level output, counting it as submitted and carrying it on the part, matching Part.tsx and outcome.ts. Covered by 'reports a legacy retrieval output as submitted'.

Comment on lines +347 to +350
if (failure) {
state = 'output-error';
} else if (submitted || runStepStatus === 'completed' || (progress ?? 0) >= 1) {
state = 'output-available';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify recognized tool-error outputs as failures

When an older persisted tool call has an error-prefixed output but no durable failure marker, submitted wins here and produces output-available. Existing rendering explicitly treats those strings as failures via OutputRenderer.tsx:25-27 and ToolCall.tsx:162, so the UI-parts view turns a restored failure into success; include the established output-error predicate when deriving this state.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. An output that isToolErrorOutput recognises (the tool-call error prefix or 'Error processing tool', the same rule as the client's isError) maps to output-error. The predicate now lives in data-provider errors.ts. Covered by 'reports an error-prefixed output as output-error'.

Comment thread packages/data-provider/src/parts.ts Outdated
}
}
const files = attachments.map((part) => {
const stored = byPath.get(part.url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match edited attachments without the mutable URL

When a UI edit changes an attachment part's url, looking up the stored record by that already-edited value cannot find the original file. The code then creates a minimal replacement and drops file_id, ownership, storage metadata, and other fields instead of applying the filepath change that applyFileEdits advertises; retain a stable original identity in the part metadata or otherwise match the original occurrence before applying the new URL.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. Attachment parts carry providerMetadata.librechat { source: 'attachment', filepath }, the path they were viewed with, and reconcileFiles matches on that identity, so an edited url still finds the stored file and keeps file_id. Covered by 'matches an attachment whose url was edited to its stored file'.

Comment thread packages/data-provider/src/parts.ts Outdated
conversationId: message.conversationId,
parentMessageId: message.parentMessageId,
...(message.sender !== undefined && { sender: message.sender }),
...(message.model != null && { model: message.model }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve nullable model metadata

For a schema-valid message with model: null, the base-less round trip fromUIMessage(toUIMessage(message)) omits the field because this guard excludes null, even though pickMessageFields can restore it. The fresh evidence in the current head is this newly added != null guard after metadata restoration was introduced; use an undefined-only check so explicit null remains distinguishable from absence.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. toUIMessage keeps model when it is defined, null included, so a base-less rebuild restores model: null. Covered by 'keeps a null model through a base-less rebuild'.

Comment on lines +523 to +526
};

const fromToolPart = (part: UIToolPart): TMessageContentParts => {
const stored = part.callProviderMetadata?.librechat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply audio media-type edits to the stored format

When a mapped input-audio part is edited from one audio type to another, this branch reads the new bytes from part.url but always restores the original metadata format. For example, changing both fields to an audio/mp3 data URL still returns { format: 'wav' }, causing downstream consumers to interpret the replacement payload using the wrong encoding; derive the restored format from the edited mediaType or data URL.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. The restored input_audio format follows an edited audio/* mediaType and falls back to the stored format otherwise. Covered by 'takes the audio format from an edited media type'.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +593 to +597
const mapped = fromUIPart(part);
if (mapped) {
content[i] = mapped;
} else if (part.type === 'step-start') {
content.length = i + 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude non-content parts from reconstructed slot indexes

When a valid UI-parts array contains a source-url or attachment file before a later content-bearing part, assigning by the UI index creates an unintended sparse hole—for example, text/source/text becomes content at indexes 0 and 2 despite having no step-start. fromUIMessage repeats the same indexing pattern, so reordered or hand-built messages shift persisted step identities; advance a separate content-slot cursor only for mapped parts and explicit step-start placeholders.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a127f34. Both reverse paths now write through one slot writer: a mapped part takes the next slot, step-start holds one open as a hole, and source-url or attachment parts take none. Covered by 'gives sources and attachments no content slot'.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a127f34343

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const { toolCall, ...partMetadata } = stored;
return { type: ContentTypes.TOOL_CALL, tool_call: toolCall, ...partMetadata };
}
const output = typeof part.output === 'string' ? part.output : part.errorText;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve array output from hand-built tool parts

When a valid hand-built tool part has a CodeInterpreterOutputs array in output and no callProviderMetadata, this ternary discards the result because it retains only strings. For example, converting an output-available tool-code_interpreter part with output: [{ logs: 'done' }] produces a stored call with no output, so the result disappears on the next mapping; reconstruct the code-interpreter shape or otherwise retain the array in a durable representation.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d9f4f2d. A hand-built tool part with an array output becomes a code_interpreter call carrying the outputs, so the result survives the next mapping. Covered by 'keeps an array output from a hand-built tool part'.

Comment on lines +581 to +582
...(output !== undefined && { output }),
...(part.state === 'output-error' && { runStepStatus: 'failed' as const }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist successful state for output-less tool parts

When a hand-built part explicitly has state: 'output-available' but its successful result is empty or omitted, this reverse mapping records no completion marker; an empty string is stored but readToolCall treats it as unsubmitted, while an omitted result is not stored at all. Mapping the rebuilt call again therefore changes it to input-available or input-streaming; persist a completed status for the explicit successful state, just as output-error persists a failed status here.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d9f4f2d. A hand-built output-available tool part now records runStepStatus: 'completed', mirroring output-error's 'failed', so it maps back as output-available with or without output. Covered by 'keeps a successful hand-built tool part successful'.

parts.push(filePart);
}
}
pushSourceParts(parts, message.attachments);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve attachments in base-less message rebuilds

When fromUIMessage(toUIMessage(message)) is called without a stored base, this projects only web-search links from message.attachments; the reverse mapper ignores source-url parts and metadata carries no original attachments, so the entire field disappears. Web citation payloads are therefore lost, while memory, workspace, and other non-web attachments disappear without even a UI part; retain the original attachment records in provider metadata for the documented base-less rebuild path.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d9f4f2d. toUIMessage carries the stored attachments in metadata.attachments, and a base-less fromUIMessage restores them. Covered by 'keeps attachments and an empty content array', a strict round-trip.

const steers: UIMessageMetadata['steers'] = [];
const summaries: UIMessageMetadata['summaries'] = [];

const contentless = !content || (content.length === 0 && (message.text?.length ?? 0) > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain explicit empty content arrays without a base

For a message with content: [] and nonempty text, this uses the same contentless marker as a message whose content field is absent, and a base-less fromUIMessage therefore rebuilds it with content: undefined. Those shapes are behaviorally distinct—for example, hasEditablePart treats missing content as an editable plain-text message but an empty array as having no editable parts—so metadata must record that the empty array existed and restore it.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d9f4f2d. metadata.emptyContent marks a contentless view whose message stored content: [], and a base-less rebuild restores the empty array. Covered by the same strict round-trip test.

Comment thread packages/data-provider/src/parts.ts Outdated
type: part.mediaType,
};
});
return files.concat(hidden);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep hidden files in their original array positions

When base.files contains an entry without filepath before or between visible files, reconciliation moves that entry to the end because every hidden record is collected separately and appended after the mapped attachments. Thus even an unchanged fromUIMessage(toUIMessage(message), message) transforms [hidden, visible] into [visible, hidden], altering persisted attachment order for downstream consumers that iterate message.files; reconcile visible records into their original slots instead of suffixing all hidden records.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d9f4f2d. Files the view never showed keep their stored slots, and visible files fill the remaining slots in view order, so an unchanged round-trip keeps [hidden, visible]. Covered by 'keeps hidden files in their stored positions'.

Adds a UIMessage-shaped view of TMessage modelled on the AI SDK
(ai@7.0.114): content parts map one-to-one onto text, reasoning,
tool-<name>, file and data-* parts, holes map to step-start, and the
reverse restores the stored content so a view round-trips.
An empty content array with text now views as contentless, an edited text part updates the stored text, attachment file parts reconcile message files, and metadata fields restore a message rebuilt without a stored base.
Tool parts fall back from an empty id to the host step id, surface approval pauses and background-cancel or validation failures, and treat an empty legacy function output as submitted. The reverse mapping writes file and attachment edits over the stored records, keeps an explicit null parent, carries non-derived message text and absent text fields, persists hand-built tool failures, and collects text, content and attachments in one pass. Web search sources include link references and take message-wide ids. UIMessage drops the system role it could not round-trip. Adds mock-harness scenarios that view persisted replies, reasoning and tool turns and map them back.
Persisted reasoning is stored inside <think> tags that every renderer strips; the reasoning part now exposes the text alone and keeps the exact wrapper so the reverse mapping restores it. The tool-turn scenario skips mobile, where the composer MCP picker is not offered.
…comes

Attachment file parts carry the stored path they were viewed with, so an edited url still matches its file. Reverse slots advance only for content parts, audio takes its format from an edited media type, a null model survives a base-less rebuild, legacy retrieval outputs count as submitted, and error-prefixed outputs map to output-error through isToolErrorOutput, the predicate the tool output renderers use.
…ugh a rebuild

A base-less rebuild restores the stored attachments and an explicit empty content array from metadata. Hand-built tool parts record a completed status for output-available and keep an array output as a code interpreter call. Files the view never showed keep their stored positions while visible files fill the others in view order.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9f4f2df80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated
if (runStepStatus === 'failed' || runStepStatus === 'cancelled') {
return runStepStatus;
}
if (typeof output === 'string' && isToolErrorOutput(output)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify memory-tool failure prose as errors

When a persisted set_memory or delete_memory call returns failure prose such as Invalid key ... without a failed run-step marker, isToolErrorOutput does not recognize it, so the submitted output maps to output-available. The existing MemoryCall.tsx:16-23 deliberately classifies every non-success memory response as a failure, meaning the UI-parts view reports restored memory failures as successes; include that established memory predicate in failure derivation.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13b2e3e by moving the rule to its owner. Memory failure prose is a client rule (MemoryCall.tsx), and data-provider cannot import it without copying it. toUIMessage/toUIParts/toUIPart now take a resolveToolFailure option, so the client applies its own predicate, and the default stays with the markers stored on the call. Covered by 'applies a failure the caller resolves from its own rules'.

input: typeof part.input === 'string' ? part.input : JSON.stringify(part.input ?? ''),
outputs: part.output,
},
...(part.state === 'output-error' && { runStepStatus: 'failed' as const }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark empty code-interpreter outputs completed

When a hand-built tool-code_interpreter explicitly has state: 'output-available' and output: [], this branch stores an empty outputs array without a completed marker; readToolCall then treats it as unsubmitted and maps it back to input-streaming. Fresh evidence in the current head is that this array-output branch records only the failed state here, while the completed marker at line 602 is unreachable for arrays. Persist runStepStatus: 'completed' for this branch as well.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13b2e3e. The hand-built array-output branch now records runStepStatus: 'completed' for output-available, so an empty outputs array maps back as output-available. Covered by 'marks an empty hand-built array output completed'.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +430 to +431
const { value } = splitText(part.text_delta ?? part.text);
return { type: 'text', text: value, state: 'streaming' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve ownership metadata on text deltas

When a stream-only text_delta carries agentId, groupId, or streamedIndex—all explicitly allowed by TextDeltaContentPart—this branch returns a bare text part and discards those fields. Mapping a parallel or handoff stream therefore removes the producer/lane identity from the part, and the reverse mapping cannot restore it while normalizing the discriminator to text; retain the content metadata in providerMetadata as the ordinary text branch does.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13b2e3e. A text_delta keeps its ownership fields (agentId, groupId, streamedIndex) in providerMetadata like the text branch, so the reverse mapping restores them onto the text part. Covered by 'keeps ownership metadata on a streamed text delta'.

Comment thread packages/data-provider/src/parts.ts Outdated
if (content && !contentless) {
for (let i = 0; i < content.length; i++) {
const part = content[i] as MappableContentPart | undefined;
const uiPart = toUIPart(part, i);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply background status attachments to tool state

When a detached tool keeps its benign dispatch handle and runStepStatus: 'completed', the actual error or cancellation can exist only in its correlated background_task_status attachment. The established outcome path in client/src/components/Chat/Messages/Content/outcome.ts:125-148 consumes that attachment, but this conversion maps the tool before consulting message.attachments, so the resulting UI part incorrectly reports output-available; correlate those status attachments while constructing message-level tool parts.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13b2e3e through the same resolveToolFailure option. toUIMessage passes the message, so the client's outcome.ts can correlate background_task_status attachments with the call instead of the mapping copying that logic. Covered by 'applies a failure the caller resolves from its own rules', which asserts the resolver receives the message.

Some tool outcomes are decided by rules the client owns, such as memory failure prose and background status attachments. toUIMessage, toUIParts and toUIPart take a resolveToolFailure option instead of copying those rules. Text deltas keep their ownership metadata, an empty hand-built array output is marked completed, and fromUIMessage documents that a stored message round-trips only with its base.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13b2e3e62c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts
Comment thread packages/data-provider/src/parts.ts
A dual-conversation turn seeds type '' placeholder slots, which fell through the part switch and crashed toUIMessage. Any slot without a known type now maps to step-start, and the reverse leaves a hole, the same thing persistence compaction does.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16aa523372

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +916 to +919
const byPath = new Map<string, Partial<TFile>>();
for (const file of baseFiles ?? []) {
if (file.filepath) {
byPath.set(file.filepath, file);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve file identity when paths collide

Fresh evidence beyond the earlier URL-edit thread is that this Map retains only the last record for each filepath. For a schema-valid base.files containing two entries with the same path but different file_id or metadata, both generated file parts resolve to that final record, so even an unchanged fromUIMessage(toUIMessage(message), message) replaces the first file and corrupts its identity; retain an occurrence or file_id-based identity instead of keying solely by path. CLAUDE.mdL130-L134

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a85fbdf. Attachment parts carry their index in message.files beside the viewed path, and reconcileFiles matches on both. Two stored files sharing a path stay distinct, and a hand-built part claims the first unclaimed match. Covered by 'keeps two stored files that share a path distinct', a strict round-trip.

Comment thread packages/data-provider/src/parts.ts Outdated
Comment on lines +979 to +980
if (!contentless) {
writer.write(part);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist parts added to contentless messages

When a view originated from a plain message without content, metadata.contentless remains true even if a consumer appends a hand-built tool, reasoning, data, or content-backed file part. This guard then skips every slot-backed part, and fromUIMessage returns the base with no content (or its unchanged empty array), silently discarding the addition; only the synthetic text representation should be excluded when deciding whether newly added content must be written.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a85fbdf. A contentless view writes content once it holds a slot-backed part other than text (tool, reasoning, data, content file), so an appended part persists and message text is kept. Covered by 'writes content added to a contentless message'.

Comment thread packages/data-provider/src/parts.ts Outdated
return { input: args, complete: args != null };
}
try {
return { input: JSON.parse(args) as UIToolInput, complete: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate primitive JSON before asserting the input type

When a stored tool call has a valid string argument such as "null", "42", or "true", JSON.parse succeeds but returns a value outside the declared UIToolInput union of strings and objects. The assertion therefore lets toUIPart expose input: null, a number, or a boolean while downstream consumers are type-checked as though that cannot happen, which can cause failures such as object operations on null; validate the parsed value or expand the public union to describe the actual result.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a85fbdf. Parsed tool arguments are used only when they are a JSON object; a primitive such as '42' or 'null' stays its string, inside the declared UIToolInput union. Covered by 'keeps primitive JSON arguments as their string'.

…plain messages

Attachment parts carry their index in message.files beside the path, so two files sharing a path stay distinct, and a hand-built part claims the first unclaimed match. Content parts added to a view of a plain message are written as content. Tool arguments that parse to a JSON primitive stay their string, inside the declared input type.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a85fbdfb89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/parts.ts Outdated

return {
type: `tool-${name}`,
toolCallId: id || `${ContentTypes.TOOL_CALL}-${index}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the streamed index for generated tool IDs

When a tool call has neither a provider id nor a stepId, this fallback uses the compacted content index even if preserveStreamedContentIdentity has stamped the outer part with its original streamedIndex. After a sparse streamed response settles into compacted content, the same invocation can therefore change from (for example) tool_call-3 to tool_call-2, defeating the stamp's purpose and causing state keyed by toolCallId to treat it as a different tool; generate the fallback from part.streamedIndex ?? index.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a496143. The fallback id is tool_call-<streamedIndex ?? index>, so a call without provider or step id keeps its identity after compaction. Covered by 'derives a fallback tool call id from the streamed index'.

Comment thread packages/data-provider/src/parts.ts Outdated
if (shape.textAbsent && value === '') {
return undefined;
}
return shape.textObject ? { ...shape.textObject, value } : value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop stale annotations when text changes

When an object-backed text part has file annotations and its UI text is edited, this unconditionally combines the new value with the original annotations. Their start_index, end_index, and annotated text still describe the old value, so fromUIPart persists citations pointing at unrelated ranges. Carry the original value in metadata and preserve annotations only while it is unchanged, or clear/rebase them when applying an edit.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a496143. Object-form text records the value its annotations index into; if the view's text differs, the annotations are dropped and an untouched part still round-trips strictly. Covered by 'drops annotations once their text is edited'.

Comment thread packages/data-provider/src/parts.ts
…annotations

A tool call with neither a provider id nor a step id takes its fallback id from the streamed index the settled message keeps, so its identity survives compaction. Annotations on object-form text are dropped once the view's text no longer matches the value they index into.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4961438ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
case ContentTypes.TEXT_DELTA: {
const { type: _type, text, text_delta: textDelta, ...rest } = part;
const { value } = splitText(textDelta ?? text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve metadata from object-valued text deltas

Beyond the earlier outer ownership fix, a supported text_delta whose payload is a TextData object still loses the object's annotations and embedded metadata because only value is retained here. For example, a streaming delta containing a file citation becomes a plain string-backed text part, so the citation disappears from the UI and cannot be restored; carry the selected payload's text shape into providerMetadata as the ordinary text branch does.

Useful? React with 👍 / 👎.

Comment on lines +654 to +656
...(part.input !== undefined && {
args: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit input-available state for raw inputs

When a hand-built tool part explicitly has state: 'input-available' but its input is an empty or otherwise non-JSON string, this stores only that raw args value. Mapping the result forward again makes parseToolInput reject the string and changes the state to input-streaming, so an invocation declared ready appears to still be loading after a round-trip; persist enough state to distinguish complete raw input from a partial JSON fragment.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Comment on lines +1006 to +1007
} else if (part.type !== 'source-url' && part.type !== 'step-start') {
addedContent = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain new step-start slots on contentless messages

Fresh evidence beyond the earlier content-addition fix is that this condition explicitly excludes step-start: when an in-progress view originating from a contentless message adds only a step-start to reserve a content slot, the writer creates the hole but addedContent remains false, so fromUIMessage discards the entire reconstructed array. A subsequent restore therefore loses the reserved index and can shift later streamed parts; count step-start as added slot-backed content even though fromUIPart returns no value for it.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant