Skip to content

fix(deep-research): recover heavy DR report from connector widget state - #9

Merged
robotlearning123 merged 1 commit into
mainfrom
fix/deep-research-widget-retrieval
Jun 18, 2026
Merged

fix(deep-research): recover heavy DR report from connector widget state#9
robotlearning123 merged 1 commit into
mainfrom
fix/deep-research-widget-retrieval

Conversation

@robotlearning123

@robotlearning123 robotlearning123 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Root cause — heavy DR moved to a connector widget

ChatGPT migrated heavy Deep Research to the "Deep Research App" connector (pineapple URI connectors://connector_openai_deep_research). The connector runs the research server-side and renders the report in an embedded widget; it never writes the report as an assistant text node in the conversation mapping. The assistant text node for the DR turn stays 0-char.

The old _poll_dr_completion walked mapping[*].message only for assistant text nodes, found nothing non-empty, and timed out at 1800s — even though venus_widget_state.status was already completed.

The fix — recover from the hidden widget state

The full report actually lives in the conversation, but only when you ask for the hidden/widget data:

GET /backend-api/conversation/{id}?include_visually_hidden_messages=true&include_widget_state=true

This exposes the report at widget_state.report_message.content.parts[0] (the full Markdown) plus report_message.metadata.content_references (source URLs, already in the items[].url/title shape the runner renders). The widget state is delivered via two carriers, both handled by the new _dr_report_from_widget_state helper:

  1. a tool node whose text part starts with "The latest state of the widget is: {…}", and
  2. message.metadata.chatgpt_sdk.widget_state (a JSON string).

_poll_dr_completion now (a) requests the conversation with those two params and (b) checks the widget state each poll, emitting the report as a done event the moment it appears. Existing assistant-text + citation-extraction logic is untouched (fallback path preserved).

Test evidence (real, by execution)

Recovered 3 real completed reports headlessly, zero DR quota spent, through the patched code path:

[dr1] chars=45649 refs=28 head='# Digital twin platforms and technology landscape for offshore oil and gas...'
[dr2] chars=52357 refs=103 head='# Reusable Assets, Environments, and Datasets for Offshore Oil and Gas...'
[dr3] chars=51534 refs=1   head='# Market map for underwater robotics simulation and digital twins...'

(dr3 has no grouped-source list — that report wrote 128 source URLs inline in the prose; the text is complete.)

Full suite (excluding the pre-existing live-network flake test_account_status_has_subscription, which 401s identically on clean main):

45 passed, 9 skipped

Adds 4 fixture-based tests (tests/fixtures/heavy_dr_widget_state.json, real oracle, no network), incl. test_poll_completion_recovers_widget_report driving the full async poll path:

tests/test_heavy_dr_parser.py  12 passed

Scope

  • Heavy deep_research_heavy: fixed (this PR).
  • Light deep_research (model=research, SearchGPT backend): a different mechanism, not changed here. Its longest-done mitigation from 0.0.3 remains; a dedicated light-mode fix is tracked as a follow-up (see CHANGELOG Notes + SKILL.md TODO).

Bumps 0.0.3 → 0.0.4; updates the deep-research skill doc + CHANGELOG.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Deep Research heavy mode now successfully generates and returns final research reports without the timeout issues experienced in previous releases
    • Citations and source references are now completely recovered and properly displayed within heavy mode reports
    • Enhanced overall system reliability for more consistent generation and retrieval of comprehensive research information

ChatGPT moved heavy Deep Research to the "Deep Research App" connector
(connectors://connector_openai_deep_research), which renders the report in an
embedded widget and never writes it as an assistant text node. The old
_poll_dr_completion scanned only assistant text, so heavy runs timed out at
1800s with an empty report even though the research completed server-side.

The report lives in the hidden widget state (widget_state.report_message).
_poll_dr_completion now fetches the conversation with
?include_visually_hidden_messages=true&include_widget_state=true and recovers
the report (text + content_references) from either widget-state carrier — a
"The latest state of the widget is: {…}" tool node, or
message.metadata.chatgpt_sdk.widget_state — via the new
_dr_report_from_widget_state helper. Existing citation-extraction is untouched.

Verified by recovering three real completed reports headlessly
(45.6K / 52.4K / 51.5K chars, with citations). Adds 4 fixture-based tests
(real oracle, no network). Light deep_research uses a different (SearchGPT)
mechanism and is unchanged; a dedicated light-mode fix is tracked as a follow-up.

Bumps version 0.0.3 -> 0.0.4; updates deep-research skill doc + CHANGELOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes heavy Deep Research report extraction by recovering the final report from the connector widget state instead of timing out while polling assistant text nodes. It adds widget-state parsing helpers, integrates them into the polling loop, validates with tests, and documents the corrected behavior with version bumps.

Changes

Heavy Deep Research widget-state report recovery

Layer / File(s) Summary
Widget state extraction helpers
gpt2agent/sse.py
Adds _coerce_widget_state and _dr_report_from_widget_state helpers with a _WIDGET_STATE_TEXT_PREFIX constant to parse widget state from either JSON-string carriers or dict wrappers, then extract the longest report_message text and associated content_references from conversation mapping nodes.
Polling loop integration
gpt2agent/sse.py
Updates _poll_dr_completion fetch URL to request include_visually_hidden_messages=true and include_widget_state=true. Modifies polling loop to first attempt widget-state extraction; when report is found, emits optional progress update (if changed) and final done event with recovered text and references, then returns immediately. Falls back to assistant-node polling if no widget state report.
Tests and widget state fixture
tests/fixtures/heavy_dr_widget_state.json, tests/test_heavy_dr_parser.py
Adds comprehensive fixture with three widget-state carriers (carrier_a_tool_text, carrier_b_metadata with embedded chatgpt_sdk.widget_state, no_report) including plans, steps, timestamps, and citation metadata. Tests validate extraction from both carrier formats, handle missing reports, and run integration test via _poll_dr_completion against fixture backend.
Documentation and versioning
gpt2agent/skills/deep-research/SKILL.md, gpt2agent/skills/deep-research/bin/deep_research.py, CHANGELOG.md, pyproject.toml
Bumps skill version to 0.1.1 and replaces "known limitation" section with corrected "Heavy DR retrieval" explanation. Updates runner docstring and citation fallback message to reflect widget-state recovery and citation preservation. Documents fix in CHANGELOG.md v0.0.4. Bumps package version to 0.0.4.

Possibly related PRs

  • robotlearning123/gpt2agent#8: Both PRs modify heavy Deep Research polling in _poll_dr_completion to recover citations, but this PR replaces the nested-metadata approach from #8 with direct widget-state extraction via a dedicated helper.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Widget state, widget state, shining so bright,
No more polling darkness, the report sees light!
Citations recovered from that hidden place,
Deep Research now triumphs—we've won this race!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: fixing heavy Deep Research report recovery by extracting from connector widget state instead of polling assistant text nodes.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deep-research-widget-retrieval

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 and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gpt2agent/skills/deep-research/SKILL.md (1)

159-164: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale heavy-DR recovery instructions in this doc.

Lines 159-164 still direct users to recover via assistant text nodes and state heavy may be unrecoverable, which conflicts with the new widget-state recovery behavior documented above in this same file.

Suggested doc correction
--mapping[*].message` for the newest assistant text node with status
--`finished_successfully` — its `metadata.content_references` holds the citation
--URLs. NOTE: heavy DR via the connector may render an "embedded UI experience"
--and never write a fetchable report node back; in that case there is nothing to
--recover and the run must be redone in a quiet window. Wait for the rate limit to
--ease first — repeated GETs while rate-limited keep it hot.
+`mapping[*].message` for widget-state carriers and recover from
+`widget_state.report_message.content.parts[0]` plus
+`report_message.metadata.content_references` (request with
+`?include_visually_hidden_messages=true&include_widget_state=true`).
+Use assistant-text fallback only if widget state is absent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gpt2agent/skills/deep-research/SKILL.md` around lines 159 - 164, Update the
stale recovery guidance that tells users to read assistant text nodes via
BackendClient/get_conversation and inspect
mapping[*].message/metadata.content_references for citations; instead, align
this section with the new widget-state recovery behavior by removing the
assertion that heavy-DR runs are unrecoverable and add instructions to use the
widget-state recovery flow described earlier (referencing "widget-state
recovery" and the heavy-DR connector behavior) so readers are directed to the
correct recovery mechanism rather than extracting content from assistant text
nodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gpt2agent/sse.py`:
- Around line 1552-1554: The code currently emits the full widget_text in a
"progress" chunk which causes duplicate content for append-based consumers;
change it to send only the delta since last_emitted (e.g., compute delta =
widget_text[len(last_emitted):] or similar) and yield {"type":"progress","text":
delta} only when delta is non-empty, then update last_emitted to widget_text;
apply this change in the same block that currently yields the full widget_text
so the "progress" chunks match the existing delta semantics used elsewhere in
the method.

---

Outside diff comments:
In `@gpt2agent/skills/deep-research/SKILL.md`:
- Around line 159-164: Update the stale recovery guidance that tells users to
read assistant text nodes via BackendClient/get_conversation and inspect
mapping[*].message/metadata.content_references for citations; instead, align
this section with the new widget-state recovery behavior by removing the
assertion that heavy-DR runs are unrecoverable and add instructions to use the
widget-state recovery flow described earlier (referencing "widget-state
recovery" and the heavy-DR connector behavior) so readers are directed to the
correct recovery mechanism rather than extracting content from assistant text
nodes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02e6851e-4a8a-4d62-ab11-a9f2f29aa3a5

📥 Commits

Reviewing files that changed from the base of the PR and between c90116c and 2807473.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • gpt2agent/skills/deep-research/SKILL.md
  • gpt2agent/skills/deep-research/bin/deep_research.py
  • gpt2agent/sse.py
  • pyproject.toml
  • tests/fixtures/heavy_dr_widget_state.json
  • tests/test_heavy_dr_parser.py

Comment thread gpt2agent/sse.py
Comment on lines +1552 to +1554
if widget_text != last_emitted:
yield {"type": "progress", "text": widget_text}
yield {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep widget-state progress chunks delta-based.

Line 1553 emits full widget_text as progress. If seed_text is already present, append-based consumers will duplicate content. Match the existing delta semantics used elsewhere in this method.

Proposed patch
             if widget_text:
                 if widget_text != last_emitted:
-                    yield {"type": "progress", "text": widget_text}
+                    if widget_text.startswith(last_emitted):
+                        delta = widget_text[len(last_emitted) :]
+                        if delta:
+                            yield {"type": "progress", "text": delta}
+                    else:
+                        yield {"type": "progress", "text": widget_text}
+                    last_emitted = widget_text
                 yield {
                     "type": "done",
                     "text": widget_text,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if widget_text != last_emitted:
yield {"type": "progress", "text": widget_text}
yield {
if widget_text:
if widget_text != last_emitted:
if widget_text.startswith(last_emitted):
delta = widget_text[len(last_emitted) :]
if delta:
yield {"type": "progress", "text": delta}
else:
yield {"type": "progress", "text": widget_text}
last_emitted = widget_text
yield {
"type": "done",
"text": widget_text,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gpt2agent/sse.py` around lines 1552 - 1554, The code currently emits the full
widget_text in a "progress" chunk which causes duplicate content for
append-based consumers; change it to send only the delta since last_emitted
(e.g., compute delta = widget_text[len(last_emitted):] or similar) and yield
{"type":"progress","text": delta} only when delta is non-empty, then update
last_emitted to widget_text; apply this change in the same block that currently
yields the full widget_text so the "progress" chunks match the existing delta
semantics used elsewhere in the method.

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