Skip to content

fix(backend/copilot): build transcript from SDK messages (atomic full-context) - #12308

Closed
majdyz wants to merge 25 commits into
devfrom
hotfix/transcript-error
Closed

fix(backend/copilot): build transcript from SDK messages (atomic full-context)#12308
majdyz wants to merge 25 commits into
devfrom
hotfix/transcript-error

Conversation

@majdyz

@majdyz majdyz commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

🎯 Summary

Fixes CoPilot sessions losing conversation history by building transcripts directly from SDK message streams instead of reading CLI files, implementing atomic full-context storage, and eliminating race conditions.

🔧 Changes

Core Architecture: TranscriptBuilder Pattern

New approach: Build transcript from SDK messages during streaming

  • Atomic full-context: Each upload REPLACES previous with complete state
  • Race-free: No CLI file reading (eliminates known GitHub issues #15813, #30217)
  • SDK-driven: Transcript reflects actual streamed messages, not disk state

Flow:

Turn 1: Upload [user1, asst1]
Turn 2: Download [user1, asst1] → Add new → Upload [user1, asst1, user2, asst2] (REPLACES)
Turn 3: Download [user1, asst1, user2, asst2] → Add new → Upload [full context] (REPLACES)

Code Changes

File Change Impact
NEW: transcript_builder.py TranscriptBuilder class (136 lines) Core new functionality
service.py Integrate TranscriptBuilder, remove Stop hook Net -170 lines
transcript.py Remove merge/read functions Net -172 lines
security_hooks.py Remove Stop hook infrastructure Net -40 lines
Tests Update for deleted functions Net -249 lines

Net result: -382 lines of code removed, massive simplification

Specific Fixes

  1. Blank lines handling (ad70449)

    • Skip empty lines in validate_transcript() instead of treating as parse errors
    • Prevents silent data loss from transcripts with blank lines
  2. PII risk mitigation (0eddb6f)

    • Downgrade content preview logging from WARNING → DEBUG
    • Prevents logging up to 500 chars of user conversation content
  3. Session lock safety (0eddb6f)

    • Add 30s timeout to upload_transcript in finally block
    • Prevents session lock from hanging indefinitely if upload stalls
  4. Code style (042ed42)

    • Move SDK imports to top-level (TextBlock, ThinkingBlock, ToolResultBlock)
    • Follow "top-level import instead of inside function" guideline

🧪 Testing

  • ✅ All 24 transcript tests pass (transcript_test.py)
  • ✅ Added test for blank lines fix (test_blank_lines_are_skipped)
  • ✅ Removed obsolete tests for deleted functions
  • ✅ All formatting/linting checks pass

📝 Transcript == Active Context

The transcript is now truly active context:

  • Downloads FULL previous context at start of turn
  • Appends new messages during streaming
  • Uploads COMPLETE state (old + new) at end
  • Each upload is atomic (replaces previous entirely)

SDK compaction is captured: When SDK compacts internally, the transcript reflects the post-compaction state.

🔗 Related

⚠️ Breaking Changes

None - fully backward compatible with existing transcripts.

📊 Code Cleanup Summary

Metric Before After Change
Lines in service.py finally block ~80 ~20 -75%
Functions in transcript.py 10 6 -40%
Stop hook code 40 lines 0 -100%
Race condition bugs Possible Impossible
Merge complexity High (UUID matching) None

majdyz added 3 commits March 6, 2026 02:29
…anscripts and remove double upload

Two root causes for copilot "forgetting" conversation history:

1. validate_transcript() required both `type: "user"` AND `type: "assistant"`
   entries. With --resume, the user's message is passed as a CLI query
   parameter and does NOT appear in the transcript file. This caused
   read_transcript_file() to return None in the stop hook, so the transcript
   was never captured or uploaded. Confirmed via Langfuse: num_turns drops
   to 1 on subsequent turns across all 3 affected sessions.

   Fix: Only require `has_assistant` — assistant entries are the meaningful
   conversation content and are always present.

2. The success path (before the finally block) uploaded the OLD resume file
   (downloaded transcript from previous turn), then the finally block
   overwrote it with the stop hook content. This double-upload was wasteful
   and could overwrite newer data with stale data.

   Fix: Remove success path upload entirely — the finally block is the
   single source of truth for transcript uploads.
@majdyz
majdyz requested a review from a team as a code owner March 6, 2026 05:56
@majdyz
majdyz requested review from Bentlybro and kcze and removed request for a team March 6, 2026 05:56
@github-project-automation github-project-automation Bot moved this to 🆕 Needs initial review in AutoGPT development kanban Mar 6, 2026
@github-actions github-actions Bot added platform/backend AutoGPT Platform - Back end size/m labels Mar 6, 2026
@coderabbitai

coderabbitai Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Centralizes transcript upload into a per-session finally block, introduces a per-session log_prefix and turn counting, captures stop-hook transcripts from disk, propagates previous_transcript_content for merging, relaxes resume transcript validation, and exposes merge_with_previous_transcript with tests.

Changes

Cohort / File(s) Summary
Service / streaming & upload
autogpt_platform/backend/backend/copilot/sdk/service.py
Adds per-session log_prefix and turn counting; tracks previous_transcript_content and ended_with_stream_error; reads stop-hook transcript from disk; moves transcript upload to a single finally block and forwards log_prefix/previous_content into _try_upload_transcript/upload_transcript.
Transcript processing & storage
autogpt_platform/backend/backend/copilot/sdk/transcript.py
Adds merge_with_previous_transcript(new, previous, log_prefix); updates strip_progress_entries to preserve unparsable lines and reserialize only reparented entries; relaxes validate_transcript for resume flows; extends upload_transcript/download_transcript signatures to accept log_prefix and previous_content.
Tests
autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
Adds tests for merge_with_previous_transcript, assistant-only resume transcripts, reserialization/reparenting behavior, and updated validation cases.
Public API / exports
autogpt_platform/backend/backend/copilot/sdk/transcript.py
Exports merge_with_previous_transcript and updates public signatures for upload_transcript and download_transcript to include log_prefix and previous_content.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Service
    participant StopHook
    participant TranscriptStore as Storage
    rect rgba(200,230,255,0.5)
    Client->>Service: start stream_chat_completion_sdk (user messages)
    Service->>Client: stream chunks/events (use log_prefix)
    Service->>Service: compute turn, track previous_transcript_content
    end
    rect rgba(220,255,200,0.5)
    Service->>StopHook: invoke stop-hook
    StopHook-->>Service: writes transcript file to disk
    Service->>Service: read stop-hook file -> set previous_transcript_content
    end
    rect rgba(255,230,200,0.5)
    Service->>Storage: _try_upload_transcript(..., log_prefix, previous_content)
    Storage-->>Service: ack / error
    Service-->>Client: finalize response / errors
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

size/l

Suggested reviewers

  • ntindle
  • kcze

Poem

🐰 I hopped through logs with tidy flair,

I moved uploads to finally's care,
I stitched old lines with newer art,
One prefix guiding every part,
🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fixes (transcript validation, double upload, synthetic entries) and the primary goal (preventing session memory loss).
Description check ✅ Passed The description provides clear context, root causes, and fixes; it is directly related to the changeset covering validation, upload, and merge logic.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hotfix/transcript-error

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
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

Caution

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

⚠️ Outside diff range comments (1)
autogpt_platform/backend/backend/copilot/sdk/transcript.py (1)

248-262: ⚠️ Potential issue | 🟠 Major

Remove the legacy two-line gate.

upload_transcript() validates the stripped JSONL, and stripping can legitimately leave a single assistant entry when the user turn comes from the CLI arg and the rest of the lines are metadata/progress. This len(lines) < 2 check still rejects that shape, so the hotfix can still drop resume state.

Proposed fix
-    lines = content.strip().split("\n")
-    if len(lines) < 2:
-        return False
+    lines = [line for line in content.strip().split("\n") if line]
+    if not lines:
+        return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/sdk/transcript.py` around lines 248
- 262, The two-line legacy gate in validate_transcript (the check "if len(lines)
< 2: return False") should be removed because stripping JSONL can legitimately
leave a single assistant entry when the user's turn is provided via CLI; delete
that length check in validate_transcript and instead rely on the existing
per-line inspection that looks for at least one assistant message (keep the
parsing/assistant-detection logic intact), ensuring transcripts with a single
assistant entry pass validation so resume state isn't dropped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@autogpt_platform/backend/backend/copilot/sdk/transcript.py`:
- Around line 266-275: The loop over lines currently breaks out when it finds an
assistant entry, which short-circuits validation and lets malformed JSONL later
in the file slip through; instead, in the function containing the json.loads
loop keep scanning all lines after setting has_assistant (do not break) so any
subsequent JSONDecodeError still triggers the existing except path (return
False); in short, remove the break and let the loop continue, preserving the
existing json.loads exception handling and final return of has_assistant.

---

Outside diff comments:
In `@autogpt_platform/backend/backend/copilot/sdk/transcript.py`:
- Around line 248-262: The two-line legacy gate in validate_transcript (the
check "if len(lines) < 2: return False") should be removed because stripping
JSONL can legitimately leave a single assistant entry when the user's turn is
provided via CLI; delete that length check in validate_transcript and instead
rely on the existing per-line inspection that looks for at least one assistant
message (keep the parsing/assistant-detection logic intact), ensuring
transcripts with a single assistant entry pass validation so resume state isn't
dropped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac7296b9-c50f-47f2-adc5-2499afe22996

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9e066 and 60f950c.

📒 Files selected for processing (2)
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: types
  • GitHub Check: Seer Code Review
  • GitHub Check: Check PR Status
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
🧠 Learnings (3)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
🔇 Additional comments (1)
autogpt_platform/backend/backend/copilot/sdk/service.py (1)

1411-1414: Nice simplification here.

Making the finally block the only upload path gives transcript capture a single source of truth and removes the stale resume-file upload path.

Comment thread autogpt_platform/backend/backend/copilot/sdk/transcript.py
majdyz added 2 commits March 6, 2026 13:06
…alidation

- Fix test_assistant_only_no_user to assert True (was False — the old buggy behavior)
- Add test_resume_transcript_without_user_entry: simulates a real --resume
  stop hook transcript with summary + assistant entries but no user entry
- Add test_returns_content_for_resume_transcript: verifies read_transcript_file
  accepts transcripts without user entries
…e_transcript

Remove `break` after finding first assistant entry so all remaining
lines are still validated for JSON correctness. Without this, corrupted
JSONL after the first assistant entry would slip through and get
uploaded as a broken --resume file.
@majdyz
majdyz changed the base branch from master to dev March 6, 2026 06:12

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

Caution

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

⚠️ Outside diff range comments (1)
autogpt_platform/backend/backend/copilot/sdk/transcript.py (1)

248-274: ⚠️ Potential issue | 🔴 Critical

Don’t reject the stripped one-line assistant transcript.

upload_transcript() validates the output of strip_progress_entries(), not the raw stop-hook file. For the exact --resume shape this PR is fixing (queue-operation/file-history-snapshot + one assistant message), stripping leaves a single assistant JSONL line, but len(lines) < 2 still returns False. That means the upload is skipped and the session can still lose history on the next turn.

Proposed fix
 def validate_transcript(content: str | None) -> bool:
@@
     lines = content.strip().split("\n")
-    if len(lines) < 2:
-        return False
 
     has_assistant = False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/sdk/transcript.py` around lines 248
- 274, The current validate_transcript in function validate_transcript wrongly
rejects valid transcripts that are a single JSONL line containing an assistant
message due to the check "if len(lines) < 2: return False"; remove or relax that
length check so a single non-empty line is accepted and let the existing
JSON-parsing loop determine validity (i.e., only return False for
empty/whitespace content or for any JSONDecodeError, otherwise return True if
any entry.get("type") == "assistant"); this fixes the case where
upload_transcript(strip_progress_entries(...)) produces a one-line assistant
JSONL and was being skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@autogpt_platform/backend/backend/copilot/sdk/transcript.py`:
- Around line 248-274: The current validate_transcript in function
validate_transcript wrongly rejects valid transcripts that are a single JSONL
line containing an assistant message due to the check "if len(lines) < 2: return
False"; remove or relax that length check so a single non-empty line is accepted
and let the existing JSON-parsing loop determine validity (i.e., only return
False for empty/whitespace content or for any JSONDecodeError, otherwise return
True if any entry.get("type") == "assistant"); this fixes the case where
upload_transcript(strip_progress_entries(...)) produces a one-line assistant
JSONL and was being skipped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49d614d4-4283-40b2-bbce-9910d219e204

📥 Commits

Reviewing files that changed from the base of the PR and between 60f950c and 3e45a28.

📒 Files selected for processing (2)
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: types
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: Seer Code Review
  • GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (6)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/backend/**/*_test.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

autogpt_platform/backend/**/*_test.py: Always review snapshot changes with git diff before committing when updating snapshots with poetry run pytest --snapshot-update
Use pytest with snapshot testing for API responses in test files
Colocate test files with source files using the *_test.py naming convention

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
autogpt_platform/backend/**/*test*.py

📄 CodeRabbit inference engine (AGENTS.md)

Run poetry run test for backend testing (runs pytest with docker based postgres + prisma)

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
🧠 Learnings (3)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py

@autogpt-reviewer autogpt-reviewer 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.

📋 Automated Review — PR #12308

PR #12308 — fix(backend/copilot): fix transcript validation and double upload causing session memory loss
Author: majdyz | Files: service.py (-40), transcript.py (+7/-9), transcript_test.py (+30/-1)


🎯 Verdict: APPROVE_WITH_CONDITIONS

What This PR Does

Hotfix for a critical P0 bug where copilot sessions lose all conversation history after the first turn. Two root causes: (1) validate_transcript() required both user and assistant entries, but with --resume the user message is a CLI query parameter and never appears in the transcript file, causing validation to reject valid transcripts; (2) the success path uploaded a stale resume file, then the finally block overwrote it with the stop-hook content (which was None due to the validation bug). Fix relaxes validation to require only assistant entries and consolidates upload to the finally block as the single source of truth.

Specialist Findings

🛡️ Security ✅ — No security concerns. Relaxed validation still rejects empty/malformed JSON. Transcript storage paths remain keyed by (user_id, session_id). Session lock + asyncio.shield() prevent races. Net security improvement by eliminating the data-loss race condition.

🏗️ Architecture ✅ — Single upload path in finally block is the correct design. Covers all exit paths (success, exception, cancellation) with asyncio.shield() protection. Fallback chain preserved (stop-hook content → resume file). Clean separation between transcript.py (validation/IO) and service.py (lifecycle). CodeRabbit's concern about early break in the validation loop is a false flag — the loop iterates all lines.

Performance ✅ — Net positive. Eliminates one redundant GCS PUT + one disk read per successful stream (~50-200ms savings). No new overhead introduced. Minor suggestion: add break after has_assistant = True to skip unnecessary json.loads() on remaining lines.

🧪 Testing ⚠️ — Three new tests cover the validation behavioral change well (test_returns_content_for_resume_transcript, test_assistant_only_no_user flipped correctly, test_resume_transcript_without_user_entry with realistic data). Gap: No integration test for the double-upload elimination in service.py — the more dangerous half of the fix is untested. Missing edge case test: malformed JSON after a valid assistant entry.

📖 Quality ✅ — Clean -40 line removal with clear explanatory comment. Docstring accurately updated. Test naming and docstrings follow conventions. No dead code remaining.

📦 Product ✅ — Critical P0 hotfix. Every multi-turn copilot user is affected (Langfuse confirms num_turns stuck at 1). Fix is correct, regression risk is low (strictly relaxes a constraint). Ship promptly.

📬 Discussion ⚠️ — CodeRabbit flagged len(lines) < 2 at transcript.py:261 twice with escalating severity — a --resume transcript stripped by strip_progress_entries() could reduce to a single assistant line and still be rejected. Author has not responded. The break concern from CodeRabbit's first pass was fixed in commit 2. Zero human reviewers. Related PR #12303 already merged, no coordination needed.

🔎 QA ✅ — Live tested: frontend loads, signup works, copilot chat functional with multi-turn session continuity confirmed. All 27 transcript unit tests pass (3 new). 7 screenshots captured.

Conditions for Approval

  1. transcript.py:261 — Relax len(lines) < 2 guard — After strip_progress_entries(), a valid --resume transcript can have exactly 1 assistant line. The current check rejects it, potentially reproducing the exact bug this PR fixes. Change to if not lines: or if len(lines) < 1:. (Flagged by CodeRabbit twice, unaddressed)

Should Fix (Follow-up OK)

  1. transcript.py:271 — Add break after has_assistant = True to avoid unnecessary json.loads() on remaining lines (or use any() pattern)
  2. service.py — Add a unit test asserting _try_upload_transcript is called exactly once per successful turn (prevents re-introduction of double upload)
  3. transcript_test.py — Add test for malformed JSON line after a valid assistant entry (validates full-scan behavior)
  4. transcript_test.py — Add test for single-line transcript boundary (len(lines) < 2 behavior)

QA Screenshots

Step Screenshot
Landing page landing
After signup signup
Copilot page copilot
Message sent sent
Response response
Follow-up followup
Session continuity continuity

Risk Assessment

Merge risk: LOW — Pure bugfix, removes code rather than adding it, well-scoped to 2 files
Rollback: EASY — Revert restores old validation + dual upload path; no schema/migration changes

CI Status

✅ All checks pass (CodeQL, Snyk, tests 3.11/3.12/3.13, types, lint)


@ntindle Critical hotfix for copilot session memory loss — approve with one condition: relax the len(lines) < 2 guard in validate_transcript() to prevent the same bug from recurring on single-line stripped transcripts.

…sistant content

When using --resume, the CLI creates a new session and writes synthetic
placeholders (model: "<synthetic>", "No response requested.") for all
previous assistant turns. This caused the copilot to "forget" its own
answers across turns.

Changes:
- Wire up merge_with_previous_transcript in the upload pipeline: the
  downloaded transcript from the start of the turn is passed through
  to upload_transcript, which restores real assistant content before
  stripping and uploading.
- Refactor strip_progress_entries to preserve original JSON line
  formatting for entries that don't need reparenting, avoiding
  unnecessary re-serialization.
- Add structured log_prefix ([SDK][session][turn]) across all SDK
  and transcript log lines for better debugging.
- Add tests for merge logic and line-preservation behavior.
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

🔍 PR Overlap Detection

This check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early.

🔴 Merge Conflicts Detected

The following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.

Summary: 1 conflict(s), 0 medium risk, 0 low risk (out of 1 PRs with file overlap)


Auto-generated on push. Ignores: openapi.json, lock files.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@autogpt_platform/backend/backend/copilot/sdk/service.py`:
- Around line 780-784: The log prefix computes turn before the incoming user
message is appended, causing off-by-one T0/Tn logs; in
stream_chat_completion_sdk() (and the similar blocks around the later
occurrences), move the computation of turn = sum(1 for m in session.messages if
m.role == "user") and the log_prefix = f"[SDK][{session_id[:12]}][T{turn}]" to
after you append the new user message (or increment turn after append) so the
prefix reflects the current turn for session and session_id; update all
occurrences (including the block at the later 799-812 region) accordingly.
- Around line 1451-1455: The current logger.info call ("%s Stream completed
successfully with %d messages") runs even when ended_with_stream_error is true;
update the post-stream logging around logger.info to check the
ended_with_stream_error flag (used in the same function) and, if true, emit an
error/warn log that the stream ended with an error including relevant context
(e.g., log_prefix and len(session.messages)) instead of the success message;
modify the branch that references logger.info and ended_with_stream_error so
successful completion is only logged when ended_with_stream_error is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fe23b2e-b1ba-4cef-92c7-d3a29deb9260

📥 Commits

Reviewing files that changed from the base of the PR and between 3e45a28 and 7042fce.

📒 Files selected for processing (3)
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: types
  • GitHub Check: Seer Code Review
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: Check PR Status
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/backend/**/*_test.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

autogpt_platform/backend/**/*_test.py: Always review snapshot changes with git diff before committing when updating snapshots with poetry run pytest --snapshot-update
Use pytest with snapshot testing for API responses in test files
Colocate test files with source files using the *_test.py naming convention

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
autogpt_platform/backend/**/*test*.py

📄 CodeRabbit inference engine (AGENTS.md)

Run poetry run test for backend testing (runs pytest with docker based postgres + prisma)

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
🧠 Learnings (3)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript_test.py

Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py Outdated

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

♻️ Duplicate comments (2)
autogpt_platform/backend/backend/copilot/sdk/service.py (2)

780-783: ⚠️ Potential issue | 🟡 Minor

log_prefix is still one turn behind.

This is built before the new user message is appended at Lines 799-812, so normal requests still log the first turn as T0 and later turns lag by one. Recompute it after the append path, or increment when a new user message is added, so the transcript/download/upload logs stay aligned for the current turn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/sdk/service.py` around lines 780 -
783, The log_prefix is computed from turn = sum(1 for m in session.messages if
m.role == "user") before the new user message is appended, so it lags by one;
update the computation so logs reflect the current turn: either move/recompute
the turn and log_prefix after the code path that appends the new user message to
session.messages (the block that adds the incoming user message), or increment
turn by 1 when you append a user message. Adjust references to turn and
log_prefix accordingly so transcript/download/upload logs use the corrected
current-turn value.

1451-1455: ⚠️ Potential issue | 🟡 Minor

Don't log SDK stream failures as successful completions.

ended_with_stream_error can already be True here, so this still emits a success line after a failed stream and makes the new per-turn telemetry misleading.

Suggested fix
-        logger.info(
-            "%s Stream completed successfully with %d messages",
-            log_prefix,
-            len(session.messages),
-        )
+        if ended_with_stream_error:
+            logger.warning(
+                "%s Stream ended with SDK error after %d messages",
+                log_prefix,
+                len(session.messages),
+            )
+        else:
+            logger.info(
+                "%s Stream completed successfully with %d messages",
+                log_prefix,
+                len(session.messages),
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/sdk/service.py` around lines 1451 -
1455, The logger.info call reporting "%s Stream completed successfully..." runs
even when ended_with_stream_error is True; update the post-stream logging logic
in the function that uses logger.info, log_prefix, session.messages and
ended_with_stream_error to only emit the successful completion message when
ended_with_stream_error is False, and emit an appropriate error/warning log (or
skip success telemetry) when ended_with_stream_error is True so failed streams
are not recorded as successes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@autogpt_platform/backend/backend/copilot/sdk/service.py`:
- Around line 780-783: The log_prefix is computed from turn = sum(1 for m in
session.messages if m.role == "user") before the new user message is appended,
so it lags by one; update the computation so logs reflect the current turn:
either move/recompute the turn and log_prefix after the code path that appends
the new user message to session.messages (the block that adds the incoming user
message), or increment turn by 1 when you append a user message. Adjust
references to turn and log_prefix accordingly so transcript/download/upload logs
use the corrected current-turn value.
- Around line 1451-1455: The logger.info call reporting "%s Stream completed
successfully..." runs even when ended_with_stream_error is True; update the
post-stream logging logic in the function that uses logger.info, log_prefix,
session.messages and ended_with_stream_error to only emit the successful
completion message when ended_with_stream_error is False, and emit an
appropriate error/warning log (or skip success telemetry) when
ended_with_stream_error is True so failed streams are not recorded as successes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94575d55-00de-41b0-afbe-a7ef3ac3d4f8

📥 Commits

Reviewing files that changed from the base of the PR and between 7042fce and 2f57c14.

📒 Files selected for processing (1)
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: types
  • GitHub Check: Seer Code Review
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (4)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
🧠 Learnings (3)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py

@majdyz

majdyz commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

✅ Already fixed! The break statement was removed in commit 3e45a28 (before the latest commit 7042fce). The bot is commenting on old code from commit 60f950c. Our current validate_transcript() function doesn't have the break and validates all lines correctly.

@majdyz
majdyz requested a review from ntindle March 6, 2026 08:23
@majdyz majdyz changed the title fix(backend/copilot): fix transcript validation and double upload causing session memory loss fix(backend/copilot): fix transcript validation, double upload, and synthetic entries causing session memory loss Mar 6, 2026
- Fix turn number off-by-one: compute log_prefix after appending user message
- Fix stream error logging: check ended_with_stream_error before logging success
- Initialize ended_with_stream_error at function start for pyright
@majdyz

majdyz commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

Fixed all CodeRabbit review comments in commit fc48944:

  1. Turn number off-by-one (line 784): Moved log_prefix computation to after the user message is appended. Now first turn shows as T1 (not T0).

  2. Stream error logging (line 1455): Added check for ended_with_stream_error before logging "Stream completed successfully". Now logs warning when stream ends with error.

  3. Break statement (line 358): Already fixed in commit 3e45a28 - no action needed.

All tests passing ✅, pyright clean ✅

@autogpt-reviewer autogpt-reviewer 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.

📋 Automated Review — PR #12308

PR #12308 — fix(backend/copilot): fix transcript validation, double upload, and synthetic entries causing session memory loss
Author: majdyz | Files: service.py (+141/-115), transcript.py (+152/-50), transcript_test.py (+179/-1)


🎯 Verdict: APPROVE_WITH_CONDITIONS

What This PR Does

Critical hotfix for copilot sessions losing all conversation history after the first turn ("forgetting"). Three root causes: (1) validate_transcript() required both user AND assistant entries, but with --resume the user message is a CLI query parameter — never in the transcript file — so validation rejected valid transcripts and they were never uploaded; (2) the success path uploaded a stale resume file, then the finally block overwrote it with stop-hook content (which was None due to #1), losing the current turn; (3) --resume creates synthetic placeholder entries (model: "<synthetic>") that replace real assistant responses. Fix relaxes validation to require only assistant entries, consolidates upload to a single finally-block path, and adds merge_with_previous_transcript() to restore real assistant content via UUID matching.

Specialist Findings

🛡️ Security ✅ — No security concerns. Relaxed validation still rejects empty/malformed JSON. Transcript storage paths remain sanitized via _sanitize_id() (hex + hyphens only, 36 char max). Merge function operates only on server-controlled data with UUID matching. asyncio.shield() correctly protects critical persistence. Net security improvement by eliminating the data-loss race condition.

🏗️ Architecture ✅ — Single upload path in finally is the correct design — covers all exit paths (success, error, cancellation) with asyncio.shield(). Clean separation: service.py handles lifecycle, transcript.py handles transformation (merge → strip → validate → upload). Pipeline ordering is correct: merge before stripping, since synthetic entries have type: "assistant". log_prefix threading via parameters (no global state) is clean. Minor: _on_stop bypasses read_transcript_file() intentionally — well-documented with comment.

Performance ✅ — Net positive. Eliminates one full GCS round-trip per successful turn (~100-500ms savings). strip_progress_entries() now preserves original JSON lines for non-reparented entries (avoids O(n) json.dumps). Minor: merge_with_previous_transcript() double-parses JSONL (merge pass + strip pass) — could combine in future. Unbounded transcript growth over many turns is a latent risk worth monitoring.

🧪 Testing ⚠️ — 179 new test lines (12 new methods) provide solid coverage of transcript.py: validation relaxation (including single-line transcripts), merge function (happy path, guards, UUID matching, preserving real entries), and strip formatting preservation. Gap: Zero tests for service.py behavioral changes — the double-upload elimination and stop-hook rewrite are the actual "fix" but are untested. Missing edge cases: malformed JSON in previous content, multi-turn chained merge, whitespace-only previous content.

📖 Quality ✅ — Excellent "why" comments (stop hook bypass, strip formatting preservation). Good function naming and docstrings. Clean dead code removal. Minor: log_prefix not threaded to read_transcript_file, mixed %-format vs f-string logging, validate_transcript docstring slightly stale.

📦 Product ✅ — Critical P0 hotfix affecting every multi-turn copilot user. Langfuse evidence confirms systemic impact (num_turns stuck at 1). Fix is correct and backward compatible (previous_content=None skips merging). Low rollback risk. Complementary with already-merged PR #12303.

📬 Discussion ⚠️ — All 4 CodeRabbit concerns resolved in current HEAD: (1) len(lines) < 2 guard removed ✅, (2) break in validation loop removed ✅, (3) log_prefix off-by-one fixed ✅, (4) stream error logging fixed ✅. Author was responsive (~2.5h turnaround). Zero human reviewers. Merge conflict with PR #12282 (2 conflicts in service.py). CI pending on latest HEAD (fc48944).

🔎 QA ✅ — Live tested: frontend loads, signup works, copilot multi-turn session memory confirmed working across 3 turns (introduced self → asked for recall → asked for summary — all context retained). 35/35 unit tests pass. 5 screenshots captured.

QA Screenshots

Step Screenshot
Landing page landing
Copilot dashboard dashboard
Turn 1 response turn1
Turn 2 recall turn2
Turn 3 summary turn3

Conditions for Approval

  1. service.py:~1553 — Guard len(captured_transcript.raw_content) against None — If raw_content is ever None, this crashes the finally block before the upload executes. Use len(captured_transcript.raw_content or ""). (Flagged by Architect)

  2. Coordinate merge with PR #12282 — Overlap detection shows 2 conflicts (~49 lines) in copilot/sdk/service.py. Must be resolved before or at merge time.

Should Fix (Follow-up OK)

  1. service.py — Add unit test for _try_upload_transcript() verifying previous_content is forwarded and upload happens exactly once (prevents re-introduction of double upload)
  2. transcript.py — Add test for malformed JSON in previous_content (merge function handles it via try/except, but untested)
  3. transcript.py — Thread log_prefix to read_transcript_file() for full log traceability
  4. service.py — Initialize source = "none" (not "stop_hook") to avoid misleading logs when no transcript is captured
  5. transcript.py — Standardize on %-format logging (avoid f-strings in logger calls)
  6. service.py:~1000 — Add cross-reference comment in _on_stop pointing to read_transcript_file() for maintainability
  7. transcript.py — Consider combining merge + strip into a single JSONL parse pass (currently double-parses)
  8. Future — Add transcript size monitoring/warning for long-lived sessions (unbounded growth risk)

Risk Assessment

Merge risk: LOW — Pure bugfix, removes code rather than adding it, well-scoped to 2 files + tests
Rollback: EASY — Revert restores old validation + dual upload path; no schema/migration changes

CI Status

⏳ Tests (3.11/3.12/3.13) pending on latest HEAD (fc48944) — all other checks pass


@ntindle Critical P0 hotfix for copilot session memory loss — approve with 2 conditions: guard len() crash in finally block, coordinate merge with #12282. All CodeRabbit concerns resolved. QA confirms multi-turn memory works.

@majdyz
majdyz disabled auto-merge March 6, 2026 09:11
majdyz added 2 commits March 6, 2026 16:18
…tten merge logic

- Use simple model=='<synthetic>' check (not content - avoids false positives)
- Rename _is_synthetic_assistant_entry() -> _is_synthetic() (concise)
- Flatten merge_with_previous_transcript() with early returns/continues
- Use walrus operator where appropriate
- Reduce nesting and improve readability
- Keep debug logging to diagnose UUID mismatch issues

Note: SDK doesn't expose SYNTHETIC_MODEL constant - it's a CLI detail.
…ID matching

MAJOR SIMPLIFICATION: Instead of detecting '<synthetic>' marker (fragile),
just replace ANY assistant entry if its UUID exists in previous transcript.

This works because:
- Previous transcript has real content with UUIDs: a1, a2, ...
- New transcript (--resume) has placeholders with SAME UUIDs + new content with NEW UUID
- Matching UUIDs = old turns that need real content restored ✅
- Non-matching UUID = current turn's new real content, keep as-is ✅

Benefits:
- No fragile '<synthetic>' constant to maintain
- No SDK/CLI version compatibility concerns
- Simpler logic: just UUID matching
- Works even if CLI changes synthetic format

Credit: @majdyz for the insight!

@autogpt-reviewer autogpt-reviewer 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.

📋 Automated Re-review #2 — PR #12308

PR #12308 — fix(backend/copilot): fix transcript validation, double upload, and synthetic entries causing session memory loss
Author: majdyz | HEAD: 9135969 | Files: 25 (+1,169/-138)


🎯 Verdict: APPROVE

Previous conditions: ✅ BOTH RESOLVED

  • (1) Guard len(captured_transcript.raw_content) against None → raw_content: str = "" by design, len("") = 0, never crashes
  • (2) Coordinate merge with PR #12282 → Both PRs report MERGEABLE; standard merge-order coordination at merge time

New commits since last review (fc489449135969):

  • Added 3 unit tests for _try_upload_transcript (success, timeout, exception) — addresses previous should-fix
  • Added conversation_turn to Langfuse trace metadata for observability
  • Fixed metadata type to string for Langfuse compatibility

What This PR Does

Critical P0 hotfix for copilot sessions losing all conversation history after the first turn ("forgetting"). Three root causes: (1) validate_transcript() required both user AND assistant entries, but with --resume the user message is a CLI parameter — never in the transcript — so validation rejected valid transcripts; (2) success path uploaded stale resume file, then finally block overwrote with None stop-hook content; (3) --resume creates synthetic placeholder entries replacing real assistant responses. Fix relaxes validation, consolidates to single finally-block upload, and adds UUID-based merge to restore real assistant content.

Specialist Findings

🛡️ Security ✅ — No concerns. Auth properly scoped on new PATCH endpoint (user_id + session_id). _sanitize_id() prevents path traversal. Merge function uses UUID dict lookup (no eval/injection). asyncio.shield() correctly protects persistence. Relaxed validation still rejects empty/malformed JSON. conversation_turn metadata exposes only an integer count — no PII.

🏗️ Architecture ✅ — Single upload path in finally is the correct design — covers all exit paths with asyncio.shield(). Clean merge→strip→validate→upload pipeline. CapturedTranscript.raw_content: str = "" is safe — no None path exists. _on_stop bypassing read_transcript_file() is correct (defers validation to upload time). log_prefix threading via parameters (no global state) is clean.

Performance ✅ — Net positive. Eliminates one GCS PUT per turn (~100-500ms savings). strip_progress_entries() preserves original JSON for non-reparented entries. Minor: merge_with_previous_transcript() double-parses JSONL (merge pass + strip pass) — <10ms typical, acceptable. Should monitor unbounded transcript growth over many turns (no size cap currently).

🧪 Testing ✅ — Meaningfully improved. New TestTryUploadTranscript (3 tests) covers error-handling contract. Existing 12 test methods in transcript_test.py cover validation relaxation, merge function (synthetic replacement, UUID matching, real entry preservation), strip formatting. Minor gaps: no test for previous_content kwarg passthrough, malformed JSON in previous content. Double-upload fix structurally enforced (single call site) — adequate without integration test.

📖 Quality ✅ — Good code quality. Excellent inline comments explaining why (stop hook bypass, single-upload rationale). _is_synthetic helper is clean DRY extraction. Minor: mixed logging format styles (%-format vs f-string) throughout both files — recommend standardizing. Stale docstring on _update_title_async doesn't mention only_if_empty behavior.

📦 Product ✅ — Critical P0 hotfix affecting every multi-turn copilot user. Langfuse evidence confirms num_turns stuck at 1. Fix is correct and backward compatible. Note: PR scope is broader than "hotfix" — includes merged UX features (rename, new chat button, auto-title) and model additions (Claude Sonnet 4.6) from dev. This doesn't affect correctness but complicates potential rollback.

📬 Discussion ✅ — All 5 CodeRabbit review passes resolved. Author was highly responsive (~2.5h turnaround). Previous conditions both addressed. Zero human reviewers — needs human approval from @ntindle or @kcze. PR #12282 still open with overlapping changes but both report MERGEABLE.

🔎 QA ✅ — Live tested: frontend loads, signup works, copilot multi-turn session continuity confirmed (2 messages with context retained). Session rename works end-to-end. Auto-generated title not overwritten after user rename (only_if_empty guard working). "New Chat" button visible at top of sidebar. 11 screenshots captured.

QA Screenshots

Step Screenshot
Landing page landing
Dashboard dashboard
Copilot initial copilot
Message 1 response msg1
Sidebar rename rename
Rename submitted renamed
Message 2 (continuity) msg2
Storybook message storybook

Should Fix (Follow-up OK)

  1. Logging style — Standardize on %-format logging throughout service.py and transcript.py (currently mixed %-format and f-strings)
  2. Test gap — Add test verifying previous_content kwarg is forwarded through _try_upload_transcript to upload_transcript
  3. Test gap — Add test for malformed JSON in previous_content (merge handles via try/except but untested)
  4. Transcript size — Add warning log when transcript exceeds a size threshold (e.g., 2MB/500 lines) for operational visibility
  5. Docstring — Update _update_title_async docstring to mention only_if_empty behavior
  6. Double-parse — Consider combining merge + strip into single JSONL parse pass if transcripts grow large

Risk Assessment

Merge risk: LOW — Core fix removes code rather than adding it, well-scoped transcript changes
Rollback: EASY — Revert restores old validation + dual upload; no schema/migration changes

CI Status

✅ lint, integration_test, CodeQL, Snyk all pass
⏳ test (3.11/3.12/3.13), types, e2e pending on latest HEAD


@ntindle P0 hotfix for copilot session memory loss — both previous conditions resolved. All 8 specialists approve. QA confirms multi-turn sessions retain context. Ready for human approval.

…hing only

Remove fragile synthetic entry detection ("<synthetic>" string check) in favor
of simple UUID-based matching: previous transcript always wins for matching UUIDs,
new UUIDs are added.

This approach is more robust and doesn't depend on CLI implementation details
that could change. The test `test_preserves_real_entries` was updated to reflect
the new behavior since the scenario it tested (same UUID with different real content)
is not a known real-world case.
@majdyz
majdyz changed the base branch from master to dev March 6, 2026 09:40
@github-actions github-actions Bot removed documentation Improvements or additions to documentation platform/frontend AutoGPT Platform - Front end platform/blocks labels Mar 6, 2026
Comment thread autogpt_platform/backend/backend/copilot/sdk/transcript.py

@autogpt-reviewer autogpt-reviewer 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.

📋 Automated Re-review #3 — PR #12308

PR #12308 — fix(backend/copilot): fix transcript validation, double upload, and synthetic entries causing session memory loss
Author: majdyz | HEAD: 440a06a | Previous verdict: APPROVE (at 8725844)
New commits: 4 (8725844440a06a) — simplified UUID matching, debug logging, fix for CLI failing to append


🎯 Verdict: APPROVE

What Changed Since Last APPROVE

4 commits refactoring the merge logic:

  1. f19a423ca — Simplified merge to "previous always wins" for matching UUIDs (removes synthetic detection)
  2. 6c83a91ae + 8ec706c12 — Added diagnostic logging for merge debugging
  3. 440a06ad9 — Added safety net: if CLI transcript is smaller than previous, manually prepend previous content

What This PR Does (unchanged)

Critical P0 hotfix for copilot sessions losing all conversation history after the first turn. Three root causes: (1) validate_transcript() rejected valid --resume transcripts, (2) success path uploaded stale resume file then finally block overwrote with None, (3) --resume creates synthetic placeholders replacing real assistant responses. Fix relaxes validation, consolidates to single finally-block upload, and adds UUID-based merge to restore real assistant content.

Specialist Findings

🛡️ Security ✅ — No security vulnerabilities. UUID matching is safe (internally generated, not user-supplied). Size-comparison concatenation is safe (JSONL lines parsed independently). ⚠️ SHOULD FIX: content[:500] and stripped[:500] logged at WARNING level expose user conversation content (PII risk). Downgrade to DEBUG.

🏗️ Architecture ✅ — Single finally-block upload path, merge→strip→validate→upload pipeline all compose correctly. Manual append + merge interaction is architecturally sound (redundant but harmless). ⚠️ SHOULD FIX: Missing newline boundary check before concatenation — if previous_transcript_content doesn't end with \n, first line of raw_transcript joins with last line of previous, creating invalid JSON line. Add if not previous_transcript_content.endswith("\n"): previous_transcript_content += "\n". Also: document the "previous always wins" invariant (depends on CLI never modifying old assistant content).

Performance ✅ — Net positive (eliminates one GCS PUT per turn). Merge is O(P+N) linear. Manual append is O(n) single string concat on fallback path only. ⚠️ SHOULD FIX: Per-UUID replacement logging at INFO level (lines 131, 138) — O(n) log lines per turn. Demote to DEBUG; keep aggregate summary at INFO.

🧪 Testing ✅ — Test rename test_preserves_real_entriestest_previous_wins_for_matching_uuids is correct — assertion flip matches new behavior. New test_malformed_json_after_valid_assistant_returns_false is a good addition. Merge function has 5 meaningful tests. Integration gap for manual append logic is acknowledged and reasonably deferred. No blocking test issues.

📖 Quality ✅ — Good comments explaining merge strategy with concrete UUID examples. _is_synthetic helper is clean DRY. ⚠️ SHOULD FIX: new_assistant_uuids list (transcript.py:~115) is populated but never read — dead code. Mixed logging styles (%-format vs f-string) persist but improved. Content preview at WARNING level is a debug remnant.

📦 Product ✅ — Core changes directly fix real user-impacting bug. "Previous always wins" is correct — CLI only creates synthetic placeholders for old UUIDs during --resume, never legitimate updates. Manual append safety net prevents catastrophic history loss. Low rollback risk.

📬 Discussion ⚠️Zero human reviewers across all iterations. CodeRabbit auto-paused (needs @coderabbitai resume). Sentry flagged blank-line crash in validate_transcript() (json.loads("")JSONDecodeError on empty lines) — unaddressed. 2 debug commits should ideally be squashed before merge. PR #12282 still has merge conflicts (~49 lines in service.py).

🔎 QA ✅ — Live tested: frontend loads, signup works, 3-turn copilot session memory confirmed working (introduced self → asked for recall → asked for summary — all context retained). No stream errors, no lost context, no regressions. 5 screenshots captured.

QA Screenshots

Step Screenshot
Landing page landing
Copilot initial copilot
Turn 1 response turn1
Turn 2 memory recall turn2
Turn 3 session summary turn3

Should Fix (Follow-up OK)

  1. service.py:~1588 — Add newline boundary check before manual concatenation: if not previous_transcript_content.endswith("\n"): previous_transcript_content += "\n" — prevents creating malformed JSONL line at the boundary
  2. transcript.py:482-483 — Downgrade content preview logging from WARNING to DEBUG — logs up to 500 chars of user conversation content (PII risk). Flagged by 5/8 specialists
  3. transcript.py:131,138 — Downgrade per-UUID merge logging from INFO to DEBUG — O(n) log lines per turn; keep aggregate summary (line 148) at INFO
  4. transcript.py:~115 — Remove dead new_assistant_uuids list — populated but never read
  5. transcript.py:~65 — Document "previous always wins" invariant — add comment noting this depends on CLI never modifying old assistant content during --resume
  6. transcript.py:382 — Filter blank lines in validate_transcript() before json.loads() — empty lines cause JSONDecodeError → false rejection (flagged by Sentry)
  7. Squash debug commits (6c83a91ae, 8ec706c12) before merge for clean history
  8. Coordinate merge with PR #12282 — 2 conflicts (~49 lines) in copilot/sdk/service.py

Risk Assessment

Merge risk: LOW — Core fix removes code, new commits add a defensive safety net
Rollback: EASY — No schema/migration changes

CI Status

✅ All checks pass (tests 3.11/3.12/3.13, types, CodeQL, Snyk, lint)


@ntindle P0 hotfix for copilot session memory loss — APPROVE maintained. New commits add a reasonable safety net for CLI append failures. 8/8 specialists approve. QA confirms 3-turn session memory works. Needs human approval — zero human reviewers across all iterations.

majdyz added 5 commits March 6, 2026 18:44
… full-context)

Replace CLI file reading (race condition) with direct SDK message capture.
Transcript now represents COMPLETE active context, not incremental changes.

Changes:
- NEW: transcript_builder.py - TranscriptBuilder class
- REMOVED: Stop hook + file reading logic (~200 lines)
- REMOVED: merge_with_previous_transcript, read_transcript_file
- SIMPLIFIED: upload_transcript (no merge, atomic replace)
- CLEANED: Removed gap-based compression fallback

Benefits:
- Eliminates race conditions (Stop hook unreliable)
- Atomic transcript (full context always)
- -372 total lines removed
- Cleaner, more maintainable code

Transcript flow (atomic):
  Turn N: Download full context → Add new messages → Upload complete (REPLACE)
- Remove TestReadTranscriptFile class (read_transcript_file deleted)
- Remove TestMergeWithPreviousTranscript class (merge_with_previous_transcript deleted)
- Remove TestTryUploadTranscript class (_try_upload_transcript deleted)
- All remaining tests pass (23 tests in transcript_test.py)
Skip empty lines instead of treating them as parse errors,
preventing silent data loss from transcripts with blank lines.

Addresses PR comment #2894841670
Covers the fix in ad70449 that skips empty lines instead of
treating them as parse errors.
Move TextBlock, ThinkingBlock, ToolResultBlock imports from inside
_format_sdk_content_blocks to top-level, following code style guidelines
(prefer top-level imports over function-local imports).
Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py
…dd upload timeout

1. Downgrade content preview logging from WARNING to DEBUG (transcript.py:325-326)
   - Prevents logging up to 500 chars of user conversation content (PII risk)
   - Keep validation failure at WARNING, only preview at DEBUG

2. Add 30s timeout to upload_transcript in finally block (service.py:1557)
   - Prevents session lock from hanging indefinitely if upload stalls
   - Uses asyncio.timeout wrapper around asyncio.shield

Addresses PR review #3903048969 item #2 and discussion r2895449830
@majdyz majdyz changed the title fix(backend/copilot): fix transcript validation, double upload, and synthetic entries causing session memory loss fix(backend/copilot): build transcript from SDK messages (atomic full-context) Mar 6, 2026
@majdyz

majdyz commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

@autogpt-reviewer review

Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py Outdated
majdyz added 2 commits March 6, 2026 19:23
HIGH severity fix: When upload_transcript times out after 30s, the shielded
coroutine continues running but becomes orphaned (no reference). Python's GC
can reclaim the task before completion, causing silent data loss.

Fix: If TimeoutError occurs, explicitly create task and track in _background_tasks
to maintain strong reference. Upload completes in background without blocking
session lock release.

Addresses PR discussion r2895491552
Previous fix created NEW task after timeout, causing double upload:
- Original shielded task still running
- New task also uploading same transcript

Correct fix: Create task FIRST, then shield it. If timeout occurs,
track the SAME task (no double upload).

Fixes double-upload bug in b8c65e3
@majdyz majdyz closed this Mar 6, 2026
@github-project-automation github-project-automation Bot moved this from 🆕 Needs initial review to ✅ Done in AutoGPT development kanban Mar 6, 2026
@github-project-automation github-project-automation Bot moved this to Done in Frontend Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform/backend AutoGPT Platform - Back end size/m size/xl

Projects

Status: ✅ Done
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants