Skip to content

fix(acp): keep OMP replies that follow its reasoning - #26658

Merged
brennanb2025 merged 4 commits into
mainfrom
brennanb2025/omp-acp-lost-reply
Oct 9, 2026
Merged

brennanb2025 merged 4 commits into
mainfrom
brennanb2025/omp-acp-lost-reply

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

OMP could finish a reply while Orca showed only its reasoning. OMP gives the reasoning and answer the same message identifier; Orca treated them as one text stream, silently discarded the answer, and still showed a successful turn. Answers that follow reasoning now appear in chat and stay saved when the chat reopens.

What Changed

  • The shared Agent Client Protocol (ACP) translation gives reasoning and answers separate, stable rows for every ACP agent, whether the agent echoes Orca’s prompt identifier or not.
  • Both items retain the provider message's first turn. Trailing chunks for a known settled message are refused instead of becoming a new row or an answer to the next prompt.
  • Rejected text streams and known settled-message chunks emit main-process diagnostics with identifiers only. No message contents or new UI notices are added.

Why

The mistake was at the provider translation boundary. Keeping separate channel items there preserves the shared journal writer’s rule that a text row cannot change from reasoning into an answer. Message ownership is kept in bounded, process-local state for the translator's lifetime, without adding persistent fields or rewriting saved chats.

Linked Issue

Found while investigating the OMP reasoning-only reply observed in #26502. Independently reproduced from Windows OMP + Gemini frames in #26587 (omp-message-collision-repro.json): the same provider identifier appeared on reasoning and answer, and the answer was discarded despite a successful finish. That second-platform evidence exercises the real translator and assembler with a fake admitting sink; it is not live Windows validation of this fix.

Visual Proof

Before: the original #26502 chat finishes with “Worked for 3s” and no answer. The provider’s saved history contained the answer; raw wire for that historical turn was unavailable.

Original missing OMP reply

After: OMP 17.0.5 in a hidden, isolated laptop rig answered a reasoning prompt. The chat shows the "Reasoning" row and the answer in the same turn:

OMP answer shown after its reasoning

The saved chat (read directly from the rig's database) has the reasoning and the answer as two separate rows from the same OMP message, then the turn finishing successfully. After a renderer reload (not an app restart) the same chat still showed the exact answer in the page text. The screenshot taken after the reload is not attached: a first-run "Jump to a worktree" tip covered the chat in it, so that step is shown by the page text and the saved rows, not by an image.

This live check ran on a local, never-pushed test build that merged this branch at 3f52e6fe00a with #26502 at cb0556199314 (build 403242994a66), not on this branch alone. The final head 20072b9fea2 adds only a merge of current main; the change itself is identical. A second prompt in that run was never sent: the tip dialog blocked the Send click, and no submission reached the app, so it says nothing about this fix.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Scrubbed real OMP 17.0.5 recordings run through the complete protocol runtime, adapter, assembler and on-disk journal. The saved answer matches all recorded answer chunks; closing the database and opening a fresh journal preserves the complete message rows.

Synthetic interleaving and late-answer regressions cover OMP, OpenCode and Grok, including completion, cancellation, and a subsequent prompt. The diagnostic test also verifies that a failing logger cannot prevent settlement. No cross-channel identifier reuse was found in the saved Grok/OpenCode recordings audited; synthetic cases protect their shared translation paths.

Validation at 3f52e6fe00a: 8 targeted files / 61 tests, the scoped main-process typecheck, all 22 pre-push repository checks, and every selected CI job passed. After the final merge of main (head 20072b9fea2; two import/test-list conflicts with #26544, which changes a different part of the same file): 9 targeted ACP files / 77 tests, the scoped main-process typecheck, and all 22 pre-push checks pass. CI on the final head is classified in the review comment. Skipped jobs are not counted as validation. Live Linux, Windows, SSH and mobile validation has not been performed. This change adds no remote wire fields, paths, platform-specific logic or mobile-facing surface.

Review

A fresh adversarial review found that splitting the text items could let a late answer escape its original turn. The approved correction retains ownership at the provider message level. Fresh architecture and implementation reviews found no in-scope blocker after the correction.

Agent skill upstream boundary

  • Not applicable; no upstream skill source is copied or translated.

Notes

Ownership retains at most 128 recent provider messages for one translator. Eviction or a fresh translator removes that correlation; this is not durable protection against arbitrary historical live duplicates. Normal history replay remains ignored and saved rows remain readable. Reusing one provider message identifier for different actual turns is an existing provider-contract limitation, outside this fix.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Before/after screenshots or videos attached
  • Self-reviewed for correctness, security, and performance
  • Cross-platform and SSH/remote impact considered
  • Final-head checks and CI classified

@brennanb2025

brennanb2025 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Problem. OMP gives a reply's reasoning and its answer the same message identifier. Orca put both into one text row, threw the answer away when the row switched from reasoning to answer, and still marked the turn successful, so the user saw "Worked for Ns" with no answer.

What changes for the user. OMP answers that follow its reasoning now appear in the chat under the "Reasoning" row and stay in the saved chat. Nothing new is shown on screen.

What was fixed. Reasoning and answer get separate text rows, while both stay with the turn that first produced their message. Late text for a message whose turn already finished is refused instead of leaking into the next prompt. Refused text produces a main-process log line with identifiers only. A fresh adversarial review found that splitting the rows alone could let a late answer move to the next prompt; that was fixed by keeping turn ownership per provider message. Fresh architecture and implementation reviews after that found no remaining in-scope problem.

Deferred / limits. Ownership is kept in memory for the 128 most recent provider messages per chat process; after eviction or a restart that link is gone. A provider reusing one message identifier for two different turns is outside this fix.

Verified.

  • Live, on a hidden isolated laptop rig with real OMP 17.0.5: the reasoning row and the answer appear in the same turn (screenshot in the PR body). The rig's saved chat, read directly from its database, has the reasoning and answer as two rows from the same OMP message and the turn finishing successfully. After a renderer reload (not an app restart) the page still showed the exact answer. The after-reload screenshot is not attached because a first-run "Jump to a worktree" tip covered the chat; that step rests on the page text and the saved rows.
  • That live run used a local, never-pushed build merging this branch at 3f52e6fe00a with feat(acp): compact Grok, OpenCode and OMP chats #26502 at cb0556199314 (build 403242994a66), not this branch alone. A second prompt in that run never left the composer (the tip dialog blocked Send; no submission reached the app), so it is not evidence either way.
  • At 3f52e6fe00a: 61 tests in 8 targeted files, scoped main-process typecheck, 22 pre-push checks, and every selected CI job passed. Recorded real OMP output runs through the full adapter and on-disk journal, including closing and reopening the database. Completion, cancellation and next-prompt cases cover OMP, OpenCode and Grok.
  • Final head 20072b9fea2 adds only a merge of current main (two import/test-list conflicts with Show sign-in guidance for all native chat agents #26544, a different change in the same file); the PR's own diff is unchanged. On it: 77 tests in 9 targeted ACP files, scoped main-process typecheck, and all 22 pre-push checks pass. CI on 20072b9fea2: all 13 jobs that ran passed (static analysis and typecheck, all five test shards, relay integration, both packaging jobs including Windows, repository guards); 17 were skipped by path selection and are not counted.

Not verified. Live Linux, Windows, SSH and mobile. A Windows OMP recording reproduces the original bug through the real translator, but it is not a live run of this fix. App restart (as opposed to renderer reload) was not exercised live; the database close-and-reopen test covers saved rows.

…lost-reply

# Conflicts:
#	config/scripts/vitest-sqlite-runtime-files.mjs
#	src/main/acp/acp-timeline-translator.ts
@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 03:59
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

ACP text chunks now use separate identities for assistant and reasoning channels. The timeline tracks message ownership across turns and rejects identified text from settled turns. The structured lane reports dropped text through its logger. New tests cover provider message ownership, stream identity, dropped-text diagnostics, OMP recordings, and journal reopening.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 20072

The fix keeps OMP replies that follow reasoning and prevents late text from moving to another turn. The remaining risk is minor: drop diagnostics log provider message identifiers unmasked.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 10 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the primary change: preserving OMP replies that follow reasoning in ACP sessions.
Description check Passed The description is complete and directly addresses the issue, implementation, testing, visual proof, limitations, and checklist requirements. It references related issues instead of using the exact `F…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 10 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/acp/acp-text-message-ownership.test.ts (1)

104-109: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert answer ownership per turn. Both tests can pass when one turn contains the expected answer text and another completed turn has no answer.

  • src/main/acp/acp-text-message-ownership.test.ts#L104-L109: assert that Next answer belongs to the second turn.
  • src/main/acp/acp-structured-omp-recordings.test.ts#L112-L112: compare each prompt's recorded answer with rows linked to its turn.

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 676a6181-0915-44fa-915b-b2ffda602301
📥 Commits

Reviewing files that changed from the base of the PR and between 04d2ccb and 20072b9.

📒 Files selected for processing (12)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/acp/acp-session-update.ts
  • src/main/acp/acp-structured-acquire.ts
  • src/main/acp/acp-structured-lane.ts
  • src/main/acp/acp-structured-omp-recordings.test.ts
  • src/main/acp/acp-text-message-ownership.test.ts
  • src/main/acp/acp-text-stream-identity.test.ts
  • src/main/acp/acp-timeline-translator.ts
  • src/main/acp/acp-turn-messages.ts
  • src/main/acp/fixtures/omp-v17-reply-after-thought.jsonl
  • src/main/acp/fixtures/omp-v17-reply-without-thought.jsonl
  • src/main/native-chat/agent-session-timeline/provider-timeline-assembler-test-support.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

providerSessionId: this.deps.providerSessionId,
agent: this.deps.agent,
generation: this.deps.generation,
...drop

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Redact provider message IDs in text-drop diagnostics. When an ACP agent places private text in messageId, acpNamedTextKey includes that value in itemId, and this spread sends it unchanged to console and span attributes. Log a bounded hash or masked identifier instead. Keep the full key for internal row identity. Based on learnings, log masked or truncated identifiers rather than unredacted provider data.

🧰 Tools
🪛 ast-grep (0.45.3)

[warning] 172-179: Avoid logging sensitive data
Context: this.logger.warn('ACP text chunk rejected by its message stream', {
scope: acp-text-${drop.reason},
sessionId: this.deps.sessionId,
providerSessionId: this.deps.providerSessionId,
agent: this.deps.agent,
generation: this.deps.generation,
...drop
})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

Sources: Learnings, Linters/SAST tools

@brennanb2025
brennanb2025 merged commit 496a171 into main Oct 9, 2026
54 checks passed
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