Skip to content

refactor(backend/copilot): unified transcript context — extract_context_messages, mode-gated --resume, compaction-aware gap-fill - #12804

Merged
majdyz merged 42 commits into
masterfrom
fix/copilot-single-session-store
Apr 16, 2026
Merged

refactor(backend/copilot): unified transcript context — extract_context_messages, mode-gated --resume, compaction-aware gap-fill#12804
majdyz merged 42 commits into
masterfrom
fix/copilot-single-session-store

Conversation

@majdyz

@majdyz majdyz commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Why / What / How

Why: The copilot had two separate GCS paths (cli-sessions/ and chat-transcripts/), redundant function names (upload_cli_session/restore_cli_session), and no shared context strategy between modes. When switching from baseline→SDK or SDK→baseline, the receiving mode discarded the stored transcript and fell back to full DB reconstruction — loading all raw messages instead of the compacted form — causing inflated context, wasted tokens, and loss of CLI compaction summaries.

What:

  • Single GCS path (cli-sessions/) for both modes — chat-transcripts/ removed
  • Unified public API: upload_transcript / download_transcript / TranscriptDownload
  • TranscriptMode = Literal["sdk", "baseline"] persisted in .meta.json — SDK skips --resume when mode != "sdk" (baseline-written JSONL has stripped fields / synthetic IDs)
  • extract_context_messages(download, session_messages) — shared context primitive used by both SDK and baseline: reads compacted transcript content + fills only the DB gap (messages after watermark), so CLI compaction summaries are preserved across mode switches
  • Watermark fix: _jsonl_covered = transcript_msg_count + 2 when a real transcript is present, preventing false gap detection after --resume
  • Baseline gap-fill: _append_gap_to_builder converts ChatMessage → JSONL entries; no more silently discarded stale transcripts

How:

SDK turn (mode="sdk" transcript available):
  ──► --resume  [full CLI session restored natively]
  ──► inject gap prefix if DB has messages after watermark

SDK turn (mode="baseline" transcript available):
  ──► cannot --resume (synthetic CLI IDs)
  ──► extract_context_messages(download, session_messages):
        returns transcript JSONL (compacted, isCompactSummary preserved) + gap
        excludes session_messages[-1] (current turn — caller injects it separately)
  ──► format as <conversation_history> + "Now, the user says: {current}"

Baseline turn (any transcript):
  ──► _load_prior_transcript → TranscriptDownload
  ──► extract_context_messages(download, session_messages) + session_messages[-1]
        replaces full session.messages DB read
  ──► LLM messages: [compacted history + gap] + [current user turn]

Transcript unavailable — both SDK (use_resume=False) and baseline:
  ──► extract_context_messages(None, session_messages) returns session_messages[:-1]
        (all prior DB messages except the current user turn at [-1])
  ──► graceful fallback — no crash, no empty context
  ──► covers: first turn, GCS error, corrupt JSONL, missing .meta.json
  ──► next successful response uploads a fresh transcript

extract_context_messages is the shared primitive — both modes call the same function, which handles:

  • download=None (first turn, GCS unavailable) → falls back to session_messages[:-1]
  • Empty/corrupt content → falls back to session_messages[:-1]
  • bytes content (raw GCS) or str content (pre-decoded baseline path)
  • isCompactSummary=True entries → preserved so CLI compaction survives mode switches
  • Missing/corrupt .meta.jsonmessage_count defaults to 0, mode defaults to "sdk"

Why [:-1] and not all messages? session_messages[-1] is always the current user turn being handled right now. Both callers inject it separately — SDK wraps it as "Now, the user says: ...", baseline appends it as the final message in the LLM array. Returning it inside extract_context_messages would double-inject it.

Changes 🏗️

  • transcript.py: CliSessionRestoreTranscriptDownload + mode field; upload_cli_sessionupload_transcript; restore_cli_sessiondownload_transcript; add TranscriptMode, detect_gap, extract_context_messages; import ChatMessage via relative path to match service.py style
  • sdk/service.py: mode-check before --resume; _RestoreResult carries baseline_download + context_messages + transcript_content; _build_query_message accepts prior_messages override; _restore_cli_session_for_turn populates context_messages via extract_context_messages and sets transcript_content to prevent duplicate DB reconstruction; watermark fix (_jsonl_covered = transcript_msg_count + 2)
  • baseline/service.py: _load_prior_transcript returns (bool, TranscriptDownload | None); LLM context replaced with extract_context_messages(download, messages); _append_gap_to_builder + detect_gap call; upload_transcript(mode="baseline")
  • sdk/transcript.py: updated re-exports, old aliases removed
  • scripts/download_transcripts.py: updated for bytes | str content type
  • Test files: 179 tests total; transcript_test.py, baseline/transcript_integration_test.py, sdk/service_helpers_test.py, sdk/test_transcript_watermark.py, test/copilot/test_transcript_watermark.py all updated/added

Checklist 📋

For code changes:

  • I have clearly listed my changes in the PR description
  • I have made a test plan
  • I have tested my changes according to the test plan:
    • 179 unit tests pass — transcript_test, baseline/transcript_integration_test, sdk/service_helpers_test, sdk/test_transcript_watermark
    • pyright 0 errors on all changed files
    • SDK --resume path still works when mode="sdk" transcript is present
    • SDK fallback uses extract_context_messages (compacted baseline content + gap) when mode="baseline" transcript is stored — no more full DB reconstruction
    • Baseline uses extract_context_messages per turn instead of full session.messages DB read
    • isCompactSummary=True entries preserved across mode switches
    • Watermark (_jsonl_covered) fix prevents false gap detection after --resume
    • Baseline gap detection no longer silently discards stale transcripts
    • TranscriptDownload.content accepts bytes | str — backward compatible
    • Transcript unavailable (GCS error, first turn, corrupt file) gracefully falls back to session_messages[:-1] without crash — applies to both SDK and baseline paths

majdyz and others added 5 commits April 15, 2026 23:19
…subsequent turns

When a user switches from baseline (fast) mode to SDK (extended_thinking)
mode mid-session, the first SDK turn has has_history=True (prior baseline
messages in DB) but no CLI session file in storage.

The old code gated session_id on `not has_history`, so mode-switch T1
never received a session_id — the CLI generated a random ID that wasn't
uploaded under the expected key.  Every subsequent SDK turn would fail to
restore the CLI session and run without --resume, injecting the full
compressed history on each turn, causing model confusion.

Fix: set session_id whenever not using --resume (the `else` branch),
covering T1 fresh, mode-switch T1, and T2+ fallback turns.  The retry
path is updated to use `"session_id" in sdk_options_kwargs` as the
discriminator (instead of `not has_history`) so mode-switch T1 retries
also keep the session_id while T2+ retries (where T1 restored a session
file via restore_cli_session) still remove it to avoid "Session ID
already in use".
…on context loss

The Claude Code CLI auto-compacts its native session JSONL when the context
approaches the model's token limit (~200K for Sonnet).  After compaction the
detailed conversation history is replaced by a ~27K-token summary, causing
the silent context loss users see as memory failures in long sessions.

Root cause identified from production logs for session 93ecf7c9:
- T6 CLI session: 233KB / ~207K tokens (near Sonnet limit)
- T7 CLI compacted session -> ~167KB / ~47K tokens (PreCompact hook missed)
- T12 second compaction -> ~176KB / ~27K tokens (just system prompt + summary)
- T14-T21: cache_read=26714 constantly -- only system prompt visible to Claude

The same stripping we already apply to our transcript (stale thinking blocks,
progress/metadata entries) now also runs on the CLI native session file.  At
~2x the size of the stripped transcript, unstripped sessions routinely hit the
compaction threshold within 6-10 turns of a heavy Opus/thinking session.
After stripping:
- same-pod turns reuse the stripped local file (no compaction trigger)
- cross-pod turns restore the stripped GCS file (same benefit)
…location

Before this change the SDK turn cycle made two separate GCS downloads and two
uploads per turn: chat-transcripts/ (our stripped JSONL + message_count meta)
and cli-sessions/ (raw CLI session for --resume).  The chat-transcripts/ path
was introduced before --resume existed; cli-sessions/ was added in PR #12777
to enable cross-pod resume, but chat-transcripts/ was never removed.

This refactoring eliminates chat-transcripts/ from the SDK path entirely:

- message_count watermark moves to a companion cli-sessions/.meta.json,
  uploaded and downloaded in the same asyncio.gather as the session file —
  no window for divergence between them.
- TranscriptBuilder is now seeded from the restored CLI session content
  (strip_for_upload applied in-memory), replacing the separate transcript
  download.
- restore_cli_session returns CliSessionRestore | None (content + message_count)
  instead of bool, combining the two previous download operations into one.
- upload_cli_session accepts message_count and writes the companion meta.
- The same-pod early-return optimisation is removed (cross-pod fix): the
  local file may be stale from an older turn that ran on this pod while a
  newer turn ran on a different pod and uploaded to GCS.

upload_transcript / download_transcript are kept for the baseline service
which has its own separate context management path.
@majdyz
majdyz requested a review from a team as a code owner April 15, 2026 18:43
@majdyz
majdyz requested review from Bentlybro and Pwuts and removed request for a team April 15, 2026 18:43
@github-project-automation github-project-automation Bot moved this to 🆕 Needs initial review in AutoGPT development kanban Apr 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR targets the master branch but does not come from dev or a hotfix/* branch.

Automatically setting the base branch to dev.

@github-actions
github-actions Bot changed the base branch from master to dev April 15, 2026 18:43
@coderabbitai

coderabbitai Bot commented Apr 15, 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

Replace JSONL transcript storage with a bytes-based CLI session model: introduce typed TranscriptDownload with content: bytes and mode, move CLI session storage to session .jsonl + .meta.json in GCS, add disk-backed helpers in SDK service for restore/upload, update resume control flow, and adapt tests for the new semantics.

Changes

Cohort / File(s) Summary
Transcript core
autogpt_platform/backend/backend/copilot/transcript.py
Introduce TranscriptMode and change TranscriptDownload to carry content: bytes, message_count, and mode. Replace prior bucket layout with two GCS objects per session (.jsonl and .meta.json). Update upload/download to handle bytes + meta and return `TranscriptDownload
SDK service
autogpt_platform/backend/backend/copilot/sdk/service.py
Add disk-backed helpers (_write/_read/_process_cli_restore), switch resume flow to guarded download_transcript(...)_process_cli_restore(...), load stripped UTF‑8 transcript into builder on success, fallback to DB-message seeding on failure, and on teardown read CLI file bytes and call upload_transcript(..., mode="sdk").
Baseline service
autogpt_platform/backend/backend/copilot/baseline/service.py
Switch prior transcript restore/upload to new CLI-session flow: decode/strip/validate restored bytes, use restore.message_count, run detect_gap(...) and append missing turns when needed, upload bytes with mode="baseline".
SDK re-exports
autogpt_platform/backend/backend/copilot/sdk/transcript.py
Adjust re-export surface: remove old CLI-specific helpers (restore_cli_session/upload_cli_session/prefix constants) and add TranscriptMode and detect_gap to exports.
Tests (SDK & core & baseline)
.../sdk/retry_scenarios_test.py, .../sdk/mode_switch_context_test.py, .../sdk/transcript_test.py, .../transcript_test.py, .../baseline/transcript_integration_test.py, .../service_test.py
Update mocks to return typed TranscriptDownload (bytes + mode) or None; change assertions from boolean restore results to None vs TranscriptDownload; expect two storage writes on upload (session + .meta.json); update projects_base test targets and variable names (e.g., cli_session).
Minor comments
autogpt_platform/backend/backend/copilot/context.py, .../sdk/security_hooks.py
Comment references updated from _projects_base() to projects_base(); no behavior changes.

Sequence Diagram

sequenceDiagram
    participant SDK as SDK Service
    participant Trans as Transcript module
    participant GCS as GCS Storage
    participant Builder as TranscriptBuilder

    SDK->>Trans: download_transcript(user_id, session_id)
    par fetch session + meta
        Trans->>GCS: GET <session_id>.jsonl
        Trans->>GCS: GET <session_id>.meta.json
    end
    GCS-->>Trans: bytes + meta (or missing)
    Trans-->>SDK: TranscriptDownload(content=bytes, message_count, mode) or None
    SDK->>SDK: decode UTF‑8, strip_for_upload(), validate_transcript()
    alt valid restore
        SDK->>Builder: load_previous(stripped_jsonl)
        SDK->>SDK: set use_resume, transcript_msg_count
    else missing/invalid
        SDK->>SDK: build transcript from DB messages
    end
    SDK->>SDK: read CLI session bytes from disk
    SDK->>Trans: upload_transcript(user_id, session_id, content=bytes, message_count, mode)
    Trans->>GCS: PUT <session_id>.jsonl (bytes)
    Trans->>GCS: PUT <session_id>.meta.json (json with message_count, mode, uploaded_at)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Pwuts
  • Bentlybro

Poem

🐇 I nibbled bytes and tucked them neat,
Two files saved — a pairing sweet.
Meta counts snug, content stored tight,
Restore returns — resume takes flight.
Hop on, the session's ready tonight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.06% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title accurately summarizes the main refactoring: consolidating transcript storage into a single path with mode-aware resumption and shared gap-fill logic across SDK and baseline modes.
Description check ✅ Passed The PR description provides comprehensive detail about the changes: consolidates session storage by eliminating redundant chat-transcripts/ path, unifies the API with TranscriptDownload and TranscriptMode, explains the new shared extract_context_messages primitive, and documents specific code changes across all affected files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/copilot-single-session-store

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.

@github-actions github-actions Bot added size/xl platform/backend AutoGPT Platform - Back end conflicts Automatically applied to PRs with merge conflicts labels Apr 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts with the base branch, please resolve those so we can evaluate the pull request.

@github-actions

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.

⚠️ This PR has conflicts with the base branch

Conflicts will need to be resolved before merging:

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

🟢 Low Risk — File Overlap Only

These PRs touch the same files but different sections (click to expand)

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


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

Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/transcript.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/transcript_test.py
- Replace redundant elif guard with plain else in service.py restore path
- Use isinstance(…, Exception) instead of BaseException in gather error
  checks for upload_cli_session (BaseException swallows KeyboardInterrupt)
- Use explicit list side_effect in test_returns_none_when_file_not_found
  to document the two-call contract of the concurrent retrieve gather
Comment thread autogpt_platform/backend/backend/copilot/transcript_test.py
…tion test

Make two-call contract of asyncio.gather explicit: RuntimeError for session
retrieve and FileNotFoundError for meta retrieve, matching the pattern
already applied to test_returns_none_when_file_not_found_in_storage.

@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/transcript.py`:
- Around line 833-841: The meta parsing currently assumes
json.loads(meta_result.decode(...)) yields a dict and that
meta.get("message_count") is a non-negative int; update the block handling
meta_result to: decode and json.loads inside a try/except (catch
JSONDecodeError, TypeError, ValueError) and log a debug message on failure,
treat non-dict results (None, list, string) as an empty dict, then read value =
meta.get("message_count", 0) and coerce/validate it as an integer with
message_count = max(0, int(value)) (guarding int() with a try/except to fall
back to 0 on invalid types); keep existing logging via logger and variables like
meta_result, log_prefix, and message_count.
- Around line 758-773: If the session upload (session_result) fails but the
companion metadata upload (meta_result) succeeded, delete/rollback the companion
metadata to avoid inconsistent state; after detecting isinstance(session_result,
BaseException) and that meta_result is not an exception, call the storage
deletion API for the metadata using the same identifiers used in the store call
(e.g., storage.delete or equivalent with workspace_id=mwid and file_id=mfid or
filename=mfname), handle and log any deletion errors, and only then
return—ensure deletion uses the same unique symbols (mwid, mfid, mfname,
meta_result) so stale meta isn't left behind.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0698b19f-8c70-43f6-889a-1fc7ecf019f5

📥 Commits

Reviewing files that changed from the base of the PR and between 2740b2b and af8a86e.

📒 Files selected for processing (5)
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/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). (12)
  • GitHub Check: check API types
  • GitHub Check: Seer Code Review
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: end-to-end tests
  • GitHub Check: type-check (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: Check PR Status
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (3)
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

autogpt_platform/backend/**/*.py: Use poetry run ... command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like openpyxl
Use absolute imports with from backend.module import ... for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no # type: ignore, # noqa, # pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use %s for deferred interpolation in debug log statements for efficiency; use f-strings elsewhere for readability (e.g., logger.debug("Processing %s items", count) vs logger.info(f"Processing {count} items"))
Sanitize error paths by using os.path.basename() in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Use transaction=True for Redis pipelines to ensure atomicity on multi-step operations
Use max(0, value) guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...

Files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
autogpt_platform/{backend,autogpt_libs}/**/*.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/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
autogpt_platform/backend/**/*_test.py

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

autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using *_test.py naming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
Use AsyncMock from unittest.mock for async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with @pytest.mark.xfail before implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, use poetry run pytest path/to/test.py --snapshot-update; always review snapshot changes with git diff before committing

Files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12797
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1991-2021
Timestamp: 2026-04-15T13:44:31.808Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/service.py` (`_run_stream_attempt`), the pre-create block (PR `#12797`) intentionally does NOT call `state.transcript_builder.append_assistant(...)` when inserting the empty assistant placeholder into `ctx.session.messages`. The transcript is left ending at the `tool_result` entry (N entries) while `message_count` metadata is N+1. This mismatch is benign and deliberate: on the next `--resume`, the SDK sees the transcript ending at `tool_result` and correctly regenerates the assistant response. Pre-appending the assistant turn to the transcript would suppress regeneration while leaving `session.messages[-1].content = ""` permanently (worse outcome). On the gap-fallback path, `transcript_msg_count (N+1) >= msg_count-1 (N)` means no gap is injected for the empty placeholder, which is correct because injecting an empty assistant message as context would mislead the SDK. Do NOT flag this transcript/message_count discrepancy as a bug.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/copilot/pending_messages.py:52-64
Timestamp: 2026-04-14T14:36:22.396Z
Learning: In `autogpt_platform/backend/backend/copilot` (PR `#12773`, commit d7bced0c6): when draining pending messages into `session.messages`, each message's text is sanitized via `strip_user_context_tags` before persistence to prevent user-controlled `<user_context>` injection from bypassing the trusted server-side context prefix. Additionally, if `upsert_chat_session` fails after draining, the drained `PendingMessage` objects are requeued back to Redis to avoid silent message loss. Do NOT flag the drain-then-requeue pattern as redundant — it is the intentional failure-resilience strategy for the pending buffer.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12445
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1071-1072
Timestamp: 2026-03-17T06:48:26.471Z
Learning: In Significant-Gravitas/AutoGPT (autogpt_platform), the AI SDK enforces `z.strictObject({type, errorText})` on SSE `StreamError` responses, so additional fields like `retryable: bool` cannot be added to `StreamError` or serialized via `to_sse()`. Instead, retry signaling for transient Anthropic API errors is done via the `COPILOT_RETRYABLE_ERROR_PREFIX` constant prepended to persisted session messages (in `ChatMessage.content`). The frontend detects retryable errors by checking `markerType === "retryable_error"` from `parseSpecialMarkers()` — no SSE schema changes and no string matching on error text. This pattern was established in PR `#12445`, commit 64d82797b.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/copilot/workflow_import/converter.py:0-0
Timestamp: 2026-03-17T10:57:12.953Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, `autogpt_platform/backend/backend/copilot/workflow_import/converter.py` was fully rewritten (commit 732960e2d) to no longer make direct LLM/OpenAI API calls. The converter now builds a structured text prompt for AutoPilot/CoPilot instead. There is no `response.choices` access or any direct LLM client usage in this file. Do not flag `response.choices` access or LLM client initialization patterns as issues in this file.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/blocks/autopilot.py:631-638
Timestamp: 2026-04-14T07:35:09.273Z
Learning: In `autogpt_platform/backend/backend/copilot/executor/utils.py`, `CoPilotExecutionEntry` includes a `permissions: CopilotPermissions | None` field (added in PR `#12773` / commit a0184c87b9). `enqueue_copilot_turn` accepts and serializes this field into the queue entry, `_enqueue_for_recovery` in `autopilot.py` accepts and forwards `permissions` to `enqueue_copilot_turn`, and `_execute_async` in `processor.py` restores `entry.permissions` and passes it into `stream_chat_completion_sdk`/`stream_chat_completion_baseline` via `set_execution_context`. This ensures recovered sub-agent turns respect the same tool/block permission ceiling as the original in-process execution (mirroring `_merge_inherited_permissions`). Do NOT flag recovered turns as losing their permission ceiling — it is now fully propagated through the queue.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/transcript.py:47-53
Timestamp: 2026-04-03T11:14:25.653Z
Learning: In `autogpt_platform/backend/backend/copilot/transcript.py`, `TranscriptDownload` is intentionally a Python `dataclass` (not a Pydantic model). It is a simple internal data container with 3 fields (`content: str`, `message_count: int`, `uploaded_at: float`), has no validation logic, and does not cross API boundaries. Do not flag it for conversion to Pydantic — the overhead is not justified for this use case.
📚 Learning: 2026-04-15T13:44:31.808Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12797
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1991-2021
Timestamp: 2026-04-15T13:44:31.808Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/service.py` (`_run_stream_attempt`), the pre-create block (PR `#12797`) intentionally does NOT call `state.transcript_builder.append_assistant(...)` when inserting the empty assistant placeholder into `ctx.session.messages`. The transcript is left ending at the `tool_result` entry (N entries) while `message_count` metadata is N+1. This mismatch is benign and deliberate: on the next `--resume`, the SDK sees the transcript ending at `tool_result` and correctly regenerates the assistant response. Pre-appending the assistant turn to the transcript would suppress regeneration while leaving `session.messages[-1].content = ""` permanently (worse outcome). On the gap-fallback path, `transcript_msg_count (N+1) >= msg_count-1 (N)` means no gap is injected for the empty placeholder, which is correct because injecting an empty assistant message as context would mislead the SDK. Do NOT flag this transcript/message_count discrepancy as a bug.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 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/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/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/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-04-01T04:17:41.600Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12632
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-01T04:17:41.600Z
Learning: When reviewing AutoGPT Copilot tool implementations, accept that `readOnlyHint=True` (provided via `ToolAnnotations`) may be applied unconditionally to *all* tools—even tools that have side effects (e.g., `bash_exec`, `write_workspace_file`, or other write/save operations). Do **not** flag these tools for having `readOnlyHint=True`; this is intentional to enable fully-parallel dispatch by the Anthropic SDK/CLI and has been E2E validated. Only flag `readOnlyHint` issues if they conflict with the established `ToolAnnotations` behavior (e.g., missing/incorrect propagation relative to the intended annotation mechanism).

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/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/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-17T10:57:12.953Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/copilot/workflow_import/converter.py:0-0
Timestamp: 2026-03-17T10:57:12.953Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, `autogpt_platform/backend/backend/copilot/workflow_import/converter.py` was fully rewritten (commit 732960e2d) to no longer make direct LLM/OpenAI API calls. The converter now builds a structured text prompt for AutoPilot/CoPilot instead. There is no `response.choices` access or any direct LLM client usage in this file. Do not flag `response.choices` access or LLM client initialization patterns as issues in this file.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-26T07:00:03.405Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12574
File: autogpt_platform/backend/backend/copilot/sdk/transcript.py:980-990
Timestamp: 2026-03-26T07:00:03.405Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/transcript.py`, `_rechain_tail` intentionally rewrites `parentUuid` for **all** tail entries (not just the first), because a single assistant turn can span multiple consecutive JSONL entries sharing the same `message.id` (e.g., a thinking entry + a tool_use entry). Their original `parentUuid` values may reference entries that were absorbed into the compressed prefix, so sequential rechaining of the entire tail is required to maintain a valid parent→child graph. The test `test_chains_multiple_tail_entries` validates this: the second tail entry's `parentUuid` is rewritten from its original value to the uuid of the first tail entry.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-17T06:48:26.471Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12445
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1071-1072
Timestamp: 2026-03-17T06:48:26.471Z
Learning: In Significant-Gravitas/AutoGPT (autogpt_platform), the AI SDK enforces `z.strictObject({type, errorText})` on SSE `StreamError` responses, so additional fields like `retryable: bool` cannot be added to `StreamError` or serialized via `to_sse()`. Instead, retry signaling for transient Anthropic API errors is done via the `COPILOT_RETRYABLE_ERROR_PREFIX` constant prepended to persisted session messages (in `ChatMessage.content`). The frontend detects retryable errors by checking `markerType === "retryable_error"` from `parseSpecialMarkers()` — no SSE schema changes and no string matching on error text. This pattern was established in PR `#12445`, commit 64d82797b.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-03T11:14:25.653Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/transcript.py:47-53
Timestamp: 2026-04-03T11:14:25.653Z
Learning: In `autogpt_platform/backend/backend/copilot/transcript.py`, `TranscriptDownload` is intentionally a Python `dataclass` (not a Pydantic model). It is a simple internal data container with 3 fields (`content: str`, `message_count: int`, `uploaded_at: float`), has no validation logic, and does not cross API boundaries. Do not flag it for conversion to Pydantic — the overhead is not justified for this use case.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-02-04T16:49:42.490Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-04T16:49:42.490Z
Learning: Applies to autogpt_platform/backend/**/test/**/*.py : Use snapshot testing with '--snapshot-update' flag in backend tests when output changes; always review with 'git diff'

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-04-13T14:19:19.341Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12740
File: autogpt_platform/frontend/src/app/api/openapi.json:0-0
Timestamp: 2026-04-13T14:19:19.341Z
Learning: Repo: Significant-Gravitas/AutoGPT — autogpt_platform
When adding new CoPilot tool response models (e.g., ScheduleListResponse, ScheduleDeletedResponse), update backend/api/features/chat/routes.py to include them in the ToolResponseUnion so the frontend’s autogenerated openapi.json dummy export (/api/chat/schema/tool-responses) exposes them for codegen. Do not hand-edit frontend/src/app/api/openapi.json.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-08T17:28:23.439Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: autogpt_platform/backend/AGENTS.md:0-0
Timestamp: 2026-04-08T17:28:23.439Z
Learning: Applies to autogpt_platform/backend/**/*_test.py : Mock at boundaries — mock where the symbol is **used**, not where it's **defined**; after refactoring, update mock targets to match new module paths

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
📚 Learning: 2026-04-03T11:14:45.569Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/baseline/service.py:0-0
Timestamp: 2026-04-03T11:14:45.569Z
Learning: In `autogpt_platform/backend/backend/copilot/baseline/service.py`, `transcript_builder.append_user(content=message)` is called unconditionally even when the message is a duplicate that was suppressed by the `is_new_message` guard. This is intentional: the downloaded transcript may be stale (uploaded before the previous attempt persisted the message), so always appending the current user turn prevents a malformed assistant-after-assistant transcript structure. The `is_user_message` flag is still checked (`if message and is_user_message:`), so assistant-role inputs are excluded. Do NOT flag this as a bug.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-03T11:14:16.378Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/transcript_builder.py:30-34
Timestamp: 2026-04-03T11:14:16.378Z
Learning: In `autogpt_platform/backend/backend/copilot/transcript_builder.py` (and its re-export shim at `sdk/transcript_builder.py`), `TranscriptEntry.parentUuid` is typed `str` (not `str | None`) and root entries use `parentUuid=""` (empty string) to match the canonical `_messages_to_transcript` JSONL format. `_parse_entry`, `append_user`, and `append_assistant` all coerce `None` to `""`. Do NOT flag `parentUuid=""` as incorrect — it is the correct root marker. This was fixed in PR `#12623`, commit b753cb7d0b.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/retry_scenarios_test.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-14T14:36:22.396Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/copilot/pending_messages.py:52-64
Timestamp: 2026-04-14T14:36:22.396Z
Learning: In `autogpt_platform/backend/backend/copilot` (PR `#12773`, commit d7bced0c6): when draining pending messages into `session.messages`, each message's text is sanitized via `strip_user_context_tags` before persistence to prevent user-controlled `<user_context>` injection from bypassing the trusted server-side context prefix. Additionally, if `upsert_chat_session` fails after draining, the drained `PendingMessage` objects are requeued back to Redis to avoid silent message loss. Do NOT flag the drain-then-requeue pattern as redundant — it is the intentional failure-resilience strategy for the pending buffer.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
📚 Learning: 2026-04-14T07:35:09.273Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/blocks/autopilot.py:631-638
Timestamp: 2026-04-14T07:35:09.273Z
Learning: In `autogpt_platform/backend/backend/copilot/executor/utils.py`, `CoPilotExecutionEntry` includes a `permissions: CopilotPermissions | None` field (added in PR `#12773` / commit a0184c87b9). `enqueue_copilot_turn` accepts and serializes this field into the queue entry, `_enqueue_for_recovery` in `autopilot.py` accepts and forwards `permissions` to `enqueue_copilot_turn`, and `_execute_async` in `processor.py` restores `entry.permissions` and passes it into `stream_chat_completion_sdk`/`stream_chat_completion_baseline` via `set_execution_context`. This ensures recovered sub-agent turns respect the same tool/block permission ceiling as the original in-process execution (mirroring `_merge_inherited_permissions`). Do NOT flag recovered turns as losing their permission ceiling — it is now fully propagated through the queue.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-23T06:51:32.535Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:0-0
Timestamp: 2026-03-23T06:51:32.535Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, the dedicated REST endpoint `/api/import/workflow` (and its earlier form `/api/import/competitor-workflow`) for external workflow import was removed across commits 30f801a5e, 374c8cfdb, and 732960e2d. The final architecture is a frontend-only flow: file upload is handled client-side, and URL fetching (for n8n templates) uses a Next.js server action (`fetchWorkflowFromUrl`) — there is no dedicated backend HTTP endpoint and no CoPilot tool invocation for workflow import. Do not expect or require a standalone HTTP endpoint, route-level integration tests, or a CoPilot `import_workflow` tool for workflow import in this repository.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-08T17:28:23.439Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: autogpt_platform/backend/AGENTS.md:0-0
Timestamp: 2026-04-08T17:28:23.439Z
Learning: Applies to autogpt_platform/backend/**/*_test.py : When creating snapshots in tests, use `poetry run pytest path/to/test.py --snapshot-update`; always review snapshot changes with `git diff` before committing

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
🔇 Additional comments (3)
autogpt_platform/backend/backend/copilot/sdk/service.py (3)

2429-2432: Good use of asyncio.gather() after removing transcript I/O.

This keeps the remaining independent startup work parallelized, so the refactor does not give back the latency win from dropping the redundant transcript fetch.


2456-2532: Nice consolidation to a single authoritative restore path.

Using restore_cli_session(...) for both native --resume state and TranscriptBuilder seeding, with DB-message reconstruction as the fallback, keeps the SDK resume flow coherent in cross-pod cases.


3379-3386: Persisting the watermark with the CLI session upload is the right coupling.

Passing message_count=len(session.messages) ties the companion metadata to the exact session snapshot that was just persisted, which is what the new gap-fill restore flow needs.

Comment thread autogpt_platform/backend/backend/copilot/transcript.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/transcript.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/transcript.py Outdated
@majdyz
majdyz changed the base branch from dev to master April 15, 2026 18:54

@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/service.py (1)

3337-3399: ⚠️ Potential issue | 🟠 Major

Catch CancelledError here and re-raise only after releasing the lock.

In Python 3.11, asyncio.CancelledError is a subclass of BaseException, not Exception. When asyncio.shield() raises CancelledError in the awaiting task, this block's except Exception clause does not catch it. The exception propagates immediately, skipping both _cleanup_sdk_tool_results() and the subsequent lock.release() in the finally block. This strands the per-session stream lock until its TTL expires.

Suggested fix
+        upload_cancelled = False
         if (
             config.claude_agent_use_resume
             and user_id
             and sdk_cwd
             and session is not None
@@
             try:
                 await asyncio.shield(
                     upload_cli_session(
                         user_id=user_id,
                         session_id=session_id,
                         sdk_cwd=sdk_cwd,
                         message_count=len(session.messages),
                         log_prefix=log_prefix,
                     )
                 )
+            except asyncio.CancelledError:
+                upload_cancelled = True
             except Exception as cli_upload_err:
                 logger.warning(
                     "%s CLI session upload failed in finally: %s",
                     log_prefix,
                     cli_upload_err,
                 )

         try:
             if sdk_cwd:
                 await _cleanup_sdk_tool_results(sdk_cwd)
         except Exception:
             logger.warning("%s SDK cleanup failed", log_prefix, exc_info=True)
         finally:
             # Release stream lock to allow new streams for this session
             await lock.release()
+        if upload_cancelled:
+            raise
🤖 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 3337 -
3399, The except Exception around the asyncio.shield(upload_cli_session(...))
block misses asyncio.CancelledError (a BaseException in Py3.11), which can skip
_cleanup_sdk_tool_results and the lock.release in the finally; modify the
try/except to catch BaseException (or explicitly asyncio.CancelledError) as
cli_upload_err, perform the same cleanup and ensure lock.release is executed,
then re-raise CancelledError (only for that type) after cleanup; update the
upload_cli_session/asyncio.shield handling and the surrounding finally to
guarantee _cleanup_sdk_tool_results and lock.release always run before
re-raising.
♻️ Duplicate comments (2)
autogpt_platform/backend/backend/copilot/transcript.py (2)

758-779: ⚠️ Potential issue | 🟠 Major

Rollback the companion .meta.json when the session upload fails.

These two writes are concurrent, not atomic. If the .jsonl upload fails but the .meta.json upload succeeds, the next restore can pair stale session bytes with a newer message_count, which makes the gap-fill logic think more history is covered than actually exists.

Suggested fix
     session_result, meta_result = await asyncio.gather(
         storage.store(workspace_id=wid, file_id=fid, filename=fname, content=content),
         storage.store(
             workspace_id=mwid, file_id=mfid, filename=mfname, content=meta_encoded
         ),
         return_exceptions=True,
     )
     if isinstance(session_result, Exception):
+        if not isinstance(meta_result, Exception):
+            try:
+                meta_path = _build_path_from_parts((mwid, mfid, mfname), storage)
+                await storage.delete(meta_path)
+            except Exception as cleanup_err:
+                logger.warning(
+                    "%s Failed to roll back CLI session meta: %s",
+                    log_prefix,
+                    cleanup_err,
+                )
         logger.warning(
             "%s Failed to upload CLI session file: %s", log_prefix, session_result
         )
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/transcript.py` around lines 758 -
779, When the concurrent uploads via storage.store for the session (.jsonl) and
meta (.meta.json) complete, if session_result is an Exception but meta_result
succeeded, delete/rollback the uploaded meta file to avoid mismatched state;
specifically, in the branch that checks isinstance(session_result, Exception)
(and before returning) call the storage deletion API (e.g., storage.delete or
storage.remove) for the meta upload using the same identifiers used for the
meta.store call (mwid, mfid, mfname), catch and log any deletion errors via
logger.warning, then return.

825-841: ⚠️ Potential issue | 🟠 Major

Harden restore error handling and .meta.json parsing.

This path still treats BaseException as a normal retrieve failure, so CancelledError/shutdown signals can be swallowed into a silent None restore. It also assumes the decoded metadata is a dict with a usable integer message_count; a malformed or non-dict payload turns a best-effort watermark into an avoidable restore failure.

Suggested fix
-    if isinstance(content_result, BaseException):
+    if isinstance(content_result, Exception):
         logger.warning(
             "%s Failed to download CLI session: %s", log_prefix, content_result
         )
         return None

@@
-    elif isinstance(meta_result, BaseException):
+    elif isinstance(meta_result, Exception):
         logger.debug("%s Failed to load CLI session meta: %s", log_prefix, meta_result)
     else:
-        meta = json.loads(meta_result.decode("utf-8"), fallback={})
-        message_count = meta.get("message_count", 0)
+        try:
+            meta = json.loads(meta_result.decode("utf-8"), fallback={})
+        except (UnicodeDecodeError, TypeError, ValueError):
+            meta = {}
+        if isinstance(meta, dict):
+            raw_count = meta.get("message_count", 0)
+            try:
+                message_count = max(0, int(raw_count))
+            except (TypeError, ValueError):
+                message_count = 0

As per coding guidelines, use max(0, value) guards for computed values that should never be negative.

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

In `@autogpt_platform/backend/backend/copilot/transcript.py` around lines 825 -
841, The restore path must not swallow shutdown/cancellation exceptions and must
robustly parse the .meta.json blob: when content_result or meta_result is an
exception, re-raise critical signals (e.g., asyncio.CancelledError,
KeyboardInterrupt, SystemExit) instead of returning None; for other exceptions
log as before using logger and log_prefix. For meta_result, decode and parse
safely inside a try/except around json.loads (don’t rely on a non-existent
fallback parameter), ensure the parsed value is a dict before reading
"message_count", coerce message_count = max(0, int(parsed.get("message_count",
0))) with safe fallback to 0 on type/parse errors, and keep the existing
FileNotFoundError behavior for missing meta. Use the existing variable names
content_result, meta_result, message_count, logger and log_prefix to locate
where to apply these changes.
🤖 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/service.py`:
- Around line 3337-3399: The except Exception around the
asyncio.shield(upload_cli_session(...)) block misses asyncio.CancelledError (a
BaseException in Py3.11), which can skip _cleanup_sdk_tool_results and the
lock.release in the finally; modify the try/except to catch BaseException (or
explicitly asyncio.CancelledError) as cli_upload_err, perform the same cleanup
and ensure lock.release is executed, then re-raise CancelledError (only for that
type) after cleanup; update the upload_cli_session/asyncio.shield handling and
the surrounding finally to guarantee _cleanup_sdk_tool_results and lock.release
always run before re-raising.

---

Duplicate comments:
In `@autogpt_platform/backend/backend/copilot/transcript.py`:
- Around line 758-779: When the concurrent uploads via storage.store for the
session (.jsonl) and meta (.meta.json) complete, if session_result is an
Exception but meta_result succeeded, delete/rollback the uploaded meta file to
avoid mismatched state; specifically, in the branch that checks
isinstance(session_result, Exception) (and before returning) call the storage
deletion API (e.g., storage.delete or storage.remove) for the meta upload using
the same identifiers used for the meta.store call (mwid, mfid, mfname), catch
and log any deletion errors via logger.warning, then return.
- Around line 825-841: The restore path must not swallow shutdown/cancellation
exceptions and must robustly parse the .meta.json blob: when content_result or
meta_result is an exception, re-raise critical signals (e.g.,
asyncio.CancelledError, KeyboardInterrupt, SystemExit) instead of returning
None; for other exceptions log as before using logger and log_prefix. For
meta_result, decode and parse safely inside a try/except around json.loads
(don’t rely on a non-existent fallback parameter), ensure the parsed value is a
dict before reading "message_count", coerce message_count = max(0,
int(parsed.get("message_count", 0))) with safe fallback to 0 on type/parse
errors, and keep the existing FileNotFoundError behavior for missing meta. Use
the existing variable names content_result, meta_result, message_count, logger
and log_prefix to locate where to apply these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 254d2145-4d9e-4318-9f9d-3f5318240c9b

📥 Commits

Reviewing files that changed from the base of the PR and between af8a86e and 6023d3e.

📒 Files selected for processing (3)
  • autogpt_platform/backend/backend/copilot/sdk/service.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/transcript_test.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
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

autogpt_platform/backend/**/*.py: Use poetry run ... command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like openpyxl
Use absolute imports with from backend.module import ... for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no # type: ignore, # noqa, # pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use %s for deferred interpolation in debug log statements for efficiency; use f-strings elsewhere for readability (e.g., logger.debug("Processing %s items", count) vs logger.info(f"Processing {count} items"))
Sanitize error paths by using os.path.basename() in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Use transaction=True for Redis pipelines to ensure atomicity on multi-step operations
Use max(0, value) guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...

Files:

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

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

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

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

autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using *_test.py naming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
Use AsyncMock from unittest.mock for async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with @pytest.mark.xfail before implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, use poetry run pytest path/to/test.py --snapshot-update; always review snapshot changes with git diff before committing

Files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
🧠 Learnings (26)
📓 Common learnings
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12797
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1991-2021
Timestamp: 2026-04-15T13:44:31.808Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/service.py` (`_run_stream_attempt`), the pre-create block (PR `#12797`) intentionally does NOT call `state.transcript_builder.append_assistant(...)` when inserting the empty assistant placeholder into `ctx.session.messages`. The transcript is left ending at the `tool_result` entry (N entries) while `message_count` metadata is N+1. This mismatch is benign and deliberate: on the next `--resume`, the SDK sees the transcript ending at `tool_result` and correctly regenerates the assistant response. Pre-appending the assistant turn to the transcript would suppress regeneration while leaving `session.messages[-1].content = ""` permanently (worse outcome). On the gap-fallback path, `transcript_msg_count (N+1) >= msg_count-1 (N)` means no gap is injected for the empty placeholder, which is correct because injecting an empty assistant message as context would mislead the SDK. Do NOT flag this transcript/message_count discrepancy as a bug.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/copilot/pending_messages.py:52-64
Timestamp: 2026-04-14T14:36:22.396Z
Learning: In `autogpt_platform/backend/backend/copilot` (PR `#12773`, commit d7bced0c6): when draining pending messages into `session.messages`, each message's text is sanitized via `strip_user_context_tags` before persistence to prevent user-controlled `<user_context>` injection from bypassing the trusted server-side context prefix. Additionally, if `upsert_chat_session` fails after draining, the drained `PendingMessage` objects are requeued back to Redis to avoid silent message loss. Do NOT flag the drain-then-requeue pattern as redundant — it is the intentional failure-resilience strategy for the pending buffer.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/copilot/workflow_import/converter.py:0-0
Timestamp: 2026-03-17T10:57:12.953Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, `autogpt_platform/backend/backend/copilot/workflow_import/converter.py` was fully rewritten (commit 732960e2d) to no longer make direct LLM/OpenAI API calls. The converter now builds a structured text prompt for AutoPilot/CoPilot instead. There is no `response.choices` access or any direct LLM client usage in this file. Do not flag `response.choices` access or LLM client initialization patterns as issues in this file.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12445
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1071-1072
Timestamp: 2026-03-17T06:48:26.471Z
Learning: In Significant-Gravitas/AutoGPT (autogpt_platform), the AI SDK enforces `z.strictObject({type, errorText})` on SSE `StreamError` responses, so additional fields like `retryable: bool` cannot be added to `StreamError` or serialized via `to_sse()`. Instead, retry signaling for transient Anthropic API errors is done via the `COPILOT_RETRYABLE_ERROR_PREFIX` constant prepended to persisted session messages (in `ChatMessage.content`). The frontend detects retryable errors by checking `markerType === "retryable_error"` from `parseSpecialMarkers()` — no SSE schema changes and no string matching on error text. This pattern was established in PR `#12445`, commit 64d82797b.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/blocks/autopilot.py:631-638
Timestamp: 2026-04-14T07:35:09.273Z
Learning: In `autogpt_platform/backend/backend/copilot/executor/utils.py`, `CoPilotExecutionEntry` includes a `permissions: CopilotPermissions | None` field (added in PR `#12773` / commit a0184c87b9). `enqueue_copilot_turn` accepts and serializes this field into the queue entry, `_enqueue_for_recovery` in `autopilot.py` accepts and forwards `permissions` to `enqueue_copilot_turn`, and `_execute_async` in `processor.py` restores `entry.permissions` and passes it into `stream_chat_completion_sdk`/`stream_chat_completion_baseline` via `set_execution_context`. This ensures recovered sub-agent turns respect the same tool/block permission ceiling as the original in-process execution (mirroring `_merge_inherited_permissions`). Do NOT flag recovered turns as losing their permission ceiling — it is now fully propagated through the queue.
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12284
File: autogpt_platform/frontend/src/app/api/openapi.json:11897-11900
Timestamp: 2026-03-04T23:58:18.476Z
Learning: Repo: Significant-Gravitas/AutoGPT — PR `#12284`
Backend/frontend OpenAPI codegen convention: In backend/api/features/store/model.py, the StoreSubmission and StoreSubmissionAdminView models define submitted_at: datetime | None, changes_summary: str | None, and instructions: str | None with no default. This is intentional to produce “required but nullable” fields in OpenAPI (properties appear in required[] and use anyOf [type, null]). This matches Prisma’s submittedAt DateTime? and changesSummary String?. Do not flag this as a required/nullable mismatch.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12574
File: autogpt_platform/backend/backend/copilot/sdk/transcript.py:980-990
Timestamp: 2026-03-26T07:00:03.405Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/transcript.py`, `_rechain_tail` intentionally rewrites `parentUuid` for **all** tail entries (not just the first), because a single assistant turn can span multiple consecutive JSONL entries sharing the same `message.id` (e.g., a thinking entry + a tool_use entry). Their original `parentUuid` values may reference entries that were absorbed into the compressed prefix, so sequential rechaining of the entire tail is required to maintain a valid parent→child graph. The test `test_chains_multiple_tail_entries` validates this: the second tail entry's `parentUuid` is rewritten from its original value to the uuid of the first tail entry.
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12740
File: autogpt_platform/frontend/src/app/api/openapi.json:0-0
Timestamp: 2026-04-13T14:19:19.341Z
Learning: Repo: Significant-Gravitas/AutoGPT — autogpt_platform
When adding new CoPilot tool response models (e.g., ScheduleListResponse, ScheduleDeletedResponse), update backend/api/features/chat/routes.py to include them in the ToolResponseUnion so the frontend’s autogenerated openapi.json dummy export (/api/chat/schema/tool-responses) exposes them for codegen. Do not hand-edit frontend/src/app/api/openapi.json.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/baseline/service.py:0-0
Timestamp: 2026-04-03T11:14:45.569Z
Learning: In `autogpt_platform/backend/backend/copilot/baseline/service.py`, `transcript_builder.append_user(content=message)` is called unconditionally even when the message is a duplicate that was suppressed by the `is_new_message` guard. This is intentional: the downloaded transcript may be stale (uploaded before the previous attempt persisted the message), so always appending the current user turn prevents a malformed assistant-after-assistant transcript structure. The `is_user_message` flag is still checked (`if message and is_user_message:`), so assistant-role inputs are excluded. Do NOT flag this as a bug.
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12356
File: autogpt_platform/backend/backend/copilot/constants.py:9-12
Timestamp: 2026-03-10T08:39:22.025Z
Learning: In Significant-Gravitas/AutoGPT PR `#12356`, the `COPILOT_SYNTHETIC_ID_PREFIX = "copilot-"` check in `create_auto_approval_record` (human_review.py) is intentional and safe. The `graph_exec_id` passed to this function comes from server-side `PendingHumanReview` DB records (not from user input); the API only accepts `node_exec_id` from users. Synthetic `copilot-*` IDs are only ever created server-side in `run_block.py`. The prefix skip avoids a DB lookup for a `AgentGraphExecution` record that legitimately does not exist for CoPilot sessions, while `user_id` scoping is enforced at the auth layer and on the resulting auto-approval record.
📚 Learning: 2026-04-15T13:44:31.808Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12797
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1991-2021
Timestamp: 2026-04-15T13:44:31.808Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/service.py` (`_run_stream_attempt`), the pre-create block (PR `#12797`) intentionally does NOT call `state.transcript_builder.append_assistant(...)` when inserting the empty assistant placeholder into `ctx.session.messages`. The transcript is left ending at the `tool_result` entry (N entries) while `message_count` metadata is N+1. This mismatch is benign and deliberate: on the next `--resume`, the SDK sees the transcript ending at `tool_result` and correctly regenerates the assistant response. Pre-appending the assistant turn to the transcript would suppress regeneration while leaving `session.messages[-1].content = ""` permanently (worse outcome). On the gap-fallback path, `transcript_msg_count (N+1) >= msg_count-1 (N)` means no gap is injected for the empty placeholder, which is correct because injecting an empty assistant message as context would mislead the SDK. Do NOT flag this transcript/message_count discrepancy as a bug.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-02-04T16:49:42.490Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-04T16:49:42.490Z
Learning: Applies to autogpt_platform/backend/**/test/**/*.py : Use snapshot testing with '--snapshot-update' flag in backend tests when output changes; always review with 'git diff'

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-04-08T17:28:23.439Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: autogpt_platform/backend/AGENTS.md:0-0
Timestamp: 2026-04-08T17:28:23.439Z
Learning: Applies to autogpt_platform/backend/**/*_test.py : When creating snapshots in tests, use `poetry run pytest path/to/test.py --snapshot-update`; always review snapshot changes with `git diff` before committing

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 Learning: 2026-03-17T10:57:12.953Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/copilot/workflow_import/converter.py:0-0
Timestamp: 2026-03-17T10:57:12.953Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, `autogpt_platform/backend/backend/copilot/workflow_import/converter.py` was fully rewritten (commit 732960e2d) to no longer make direct LLM/OpenAI API calls. The converter now builds a structured text prompt for AutoPilot/CoPilot instead. There is no `response.choices` access or any direct LLM client usage in this file. Do not flag `response.choices` access or LLM client initialization patterns as issues in this file.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-26T07:00:03.405Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12574
File: autogpt_platform/backend/backend/copilot/sdk/transcript.py:980-990
Timestamp: 2026-03-26T07:00:03.405Z
Learning: In `autogpt_platform/backend/backend/copilot/sdk/transcript.py`, `_rechain_tail` intentionally rewrites `parentUuid` for **all** tail entries (not just the first), because a single assistant turn can span multiple consecutive JSONL entries sharing the same `message.id` (e.g., a thinking entry + a tool_use entry). Their original `parentUuid` values may reference entries that were absorbed into the compressed prefix, so sequential rechaining of the entire tail is required to maintain a valid parent→child graph. The test `test_chains_multiple_tail_entries` validates this: the second tail entry's `parentUuid` is rewritten from its original value to the uuid of the first tail entry.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
📚 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/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • 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/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-01T04:17:41.600Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12632
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-01T04:17:41.600Z
Learning: When reviewing AutoGPT Copilot tool implementations, accept that `readOnlyHint=True` (provided via `ToolAnnotations`) may be applied unconditionally to *all* tools—even tools that have side effects (e.g., `bash_exec`, `write_workspace_file`, or other write/save operations). Do **not** flag these tools for having `readOnlyHint=True`; this is intentional to enable fully-parallel dispatch by the Anthropic SDK/CLI and has been E2E validated. Only flag `readOnlyHint` issues if they conflict with the established `ToolAnnotations` behavior (e.g., missing/incorrect propagation relative to the intended annotation mechanism).

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • 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/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript_test.py
  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-03T11:14:25.653Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/transcript.py:47-53
Timestamp: 2026-04-03T11:14:25.653Z
Learning: In `autogpt_platform/backend/backend/copilot/transcript.py`, `TranscriptDownload` is intentionally a Python `dataclass` (not a Pydantic model). It is a simple internal data container with 3 fields (`content: str`, `message_count: int`, `uploaded_at: float`), has no validation logic, and does not cross API boundaries. Do not flag it for conversion to Pydantic — the overhead is not justified for this use case.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-17T06:48:26.471Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12445
File: autogpt_platform/backend/backend/copilot/sdk/service.py:1071-1072
Timestamp: 2026-03-17T06:48:26.471Z
Learning: In Significant-Gravitas/AutoGPT (autogpt_platform), the AI SDK enforces `z.strictObject({type, errorText})` on SSE `StreamError` responses, so additional fields like `retryable: bool` cannot be added to `StreamError` or serialized via `to_sse()`. Instead, retry signaling for transient Anthropic API errors is done via the `COPILOT_RETRYABLE_ERROR_PREFIX` constant prepended to persisted session messages (in `ChatMessage.content`). The frontend detects retryable errors by checking `markerType === "retryable_error"` from `parseSpecialMarkers()` — no SSE schema changes and no string matching on error text. This pattern was established in PR `#12445`, commit 64d82797b.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-03T11:14:45.569Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/baseline/service.py:0-0
Timestamp: 2026-04-03T11:14:45.569Z
Learning: In `autogpt_platform/backend/backend/copilot/baseline/service.py`, `transcript_builder.append_user(content=message)` is called unconditionally even when the message is a duplicate that was suppressed by the `is_new_message` guard. This is intentional: the downloaded transcript may be stale (uploaded before the previous attempt persisted the message), so always appending the current user turn prevents a malformed assistant-after-assistant transcript structure. The `is_user_message` flag is still checked (`if message and is_user_message:`), so assistant-role inputs are excluded. Do NOT flag this as a bug.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-14T07:35:09.273Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/blocks/autopilot.py:631-638
Timestamp: 2026-04-14T07:35:09.273Z
Learning: In `autogpt_platform/backend/backend/copilot/executor/utils.py`, `CoPilotExecutionEntry` includes a `permissions: CopilotPermissions | None` field (added in PR `#12773` / commit a0184c87b9). `enqueue_copilot_turn` accepts and serializes this field into the queue entry, `_enqueue_for_recovery` in `autopilot.py` accepts and forwards `permissions` to `enqueue_copilot_turn`, and `_execute_async` in `processor.py` restores `entry.permissions` and passes it into `stream_chat_completion_sdk`/`stream_chat_completion_baseline` via `set_execution_context`. This ensures recovered sub-agent turns respect the same tool/block permission ceiling as the original in-process execution (mirroring `_merge_inherited_permissions`). Do NOT flag recovered turns as losing their permission ceiling — it is now fully propagated through the queue.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-14T14:36:22.396Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/backend/backend/copilot/pending_messages.py:52-64
Timestamp: 2026-04-14T14:36:22.396Z
Learning: In `autogpt_platform/backend/backend/copilot` (PR `#12773`, commit d7bced0c6): when draining pending messages into `session.messages`, each message's text is sanitized via `strip_user_context_tags` before persistence to prevent user-controlled `<user_context>` injection from bypassing the trusted server-side context prefix. Additionally, if `upsert_chat_session` fails after draining, the drained `PendingMessage` objects are requeued back to Redis to avoid silent message loss. Do NOT flag the drain-then-requeue pattern as redundant — it is the intentional failure-resilience strategy for the pending buffer.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-17T07:24:34.302Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12385
File: autogpt_platform/backend/backend/copilot/rate_limit.py:0-0
Timestamp: 2026-03-17T07:24:34.302Z
Learning: In `autogpt_platform/backend/backend/copilot/rate_limit.py`, all fail-open `except` blocks catch `(RedisError, ConnectionError, OSError)` specifically — not bare `except Exception`. This applies to `_session_reset_from_ttl`, `get_usage_status`, `check_rate_limit`, and `record_token_usage`. The narrowed tuple ensures only genuine Redis/network failures are swallowed; unexpected exceptions propagate normally.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
📚 Learning: 2026-04-15T02:06:38.113Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/util/workspace.py:198-221
Timestamp: 2026-04-15T02:06:38.113Z
Learning: In `autogpt_platform/backend/backend/util/workspace.py`, the TOCTOU gap between the `get_workspace_total_size()` pre-check and the subsequent storage write + DB insert in `WorkspaceManager.write_file()` is an accepted trade-off. Reasons: (1) the workspace is single-user-scoped, so a true race requires precise concurrent timing from the same user; (2) the REST upload route (`autogpt_platform/backend/backend/api/features/workspace/routes.py`) already has a post-write quota check with soft-delete rollback as a safety net for that path; (3) adding the same rollback inside `write_file()` would couple the manager to HTTP semantics. The pre-write check catches the overwhelming majority of cases. Do NOT flag this as a blocking TOCTOU issue.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
📚 Learning: 2026-04-14T06:33:59.422Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12774
File: autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py:0-0
Timestamp: 2026-04-14T06:33:59.422Z
Learning: In `autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`, the `asyncio.wait_for()` retry loop around `AsyncSandbox.create()` (introduced in PR `#12774`) can leak up to `_SANDBOX_CREATE_MAX_RETRIES - 1` (≤2) orphaned E2B sandboxes per hang incident because `wait_for` cancels only the client-side wait while E2B may complete server-side provisioning. With the default `on_timeout="pause"` lifecycle, leaked orphaned sandboxes are **paused** (not killed) when their original `end_at` is reached and persist indefinitely until explicitly killed — there is NO automatic E2B project-level cleanup. Operators must manage these manually or via their own cleanup jobs. The sandbox_id is not accessible from the timed-out coroutine, so recovery via `AsyncSandbox.connect(sandbox_id)` is not possible at timeout. This is an intentionally accepted trade-off; a proper fix is deferred to a follow-up PR. Do NOT flag the retry loop as a blocking issue.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
📚 Learning: 2026-03-09T10:50:43.907Z
Learnt from: Bentlybro
Repo: Significant-Gravitas/AutoGPT PR: 0
File: :0-0
Timestamp: 2026-03-09T10:50:43.907Z
Learning: Repo: Significant-Gravitas/AutoGPT — File: autogpt_platform/backend/backend/blocks/llm.py
For xAI Grok models accessed via OpenRouter, the API returns `null` for `max_completion_tokens`. The convention in this codebase is to use the model's context window size as the `max_output_tokens` value in ModelMetadata. For example, Grok 3 uses 131072 (128k) and Grok 4 uses 262144 (256k). Do not flag these as incorrect max output token values.

Applied to files:

  • autogpt_platform/backend/backend/copilot/transcript.py
📚 Learning: 2026-03-23T06:51:32.535Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:0-0
Timestamp: 2026-03-23T06:51:32.535Z
Learning: In Significant-Gravitas/AutoGPT PR `#12440`, the dedicated REST endpoint `/api/import/workflow` (and its earlier form `/api/import/competitor-workflow`) for external workflow import was removed across commits 30f801a5e, 374c8cfdb, and 732960e2d. The final architecture is a frontend-only flow: file upload is handled client-side, and URL fetching (for n8n templates) uses a Next.js server action (`fetchWorkflowFromUrl`) — there is no dedicated backend HTTP endpoint and no CoPilot tool invocation for workflow import. Do not expect or require a standalone HTTP endpoint, route-level integration tests, or a CoPilot `import_workflow` tool for workflow import in this repository.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-13T14:19:19.341Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12740
File: autogpt_platform/frontend/src/app/api/openapi.json:0-0
Timestamp: 2026-04-13T14:19:19.341Z
Learning: Repo: Significant-Gravitas/AutoGPT — autogpt_platform
When adding new CoPilot tool response models (e.g., ScheduleListResponse, ScheduleDeletedResponse), update backend/api/features/chat/routes.py to include them in the ToolResponseUnion so the frontend’s autogenerated openapi.json dummy export (/api/chat/schema/tool-responses) exposes them for codegen. Do not hand-edit frontend/src/app/api/openapi.json.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-03T11:14:16.378Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/transcript_builder.py:30-34
Timestamp: 2026-04-03T11:14:16.378Z
Learning: In `autogpt_platform/backend/backend/copilot/transcript_builder.py` (and its re-export shim at `sdk/transcript_builder.py`), `TranscriptEntry.parentUuid` is typed `str` (not `str | None`) and root entries use `parentUuid=""` (empty string) to match the canonical `_messages_to_transcript` JSONL format. `_parse_entry`, `append_user`, and `append_assistant` all coerce `None` to `""`. Do NOT flag `parentUuid=""` as incorrect — it is the correct root marker. This was fixed in PR `#12623`, commit b753cb7d0b.

Applied to files:

  • autogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-04-01T14:54:01.937Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12636
File: autogpt_platform/backend/backend/copilot/sdk/service.py:0-0
Timestamp: 2026-04-01T14:54:01.937Z
Learning: In Significant-Gravitas/AutoGPT (autogpt_platform), `claude_agent_max_transient_retries` (default=3) in `ChatConfig` counts **total attempts including the initial one**, not the number of extra retries. With the pre-incremented `transient_retries >= max_transient` guard in `service.py`, a value of 3 yields 3 total stream attempts (initial + 2 retries with exponential backoff: 1s, 2s). Do NOT flag this as an off-by-one — the `>=` check is intentional.

Applied to files:

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

majdyz added a commit that referenced this pull request Apr 15, 2026
…complete sdk re-exports

- Use len(source) instead of len(prior) in _build_query_message fallback
  warning so the logged count reflects the actual source being compressed
- Add comment explaining retry path intentionally omits prior_messages
  and falls back to full DB context (authoritative, overhead acceptable)
- Add missing cli_session_path, extract_context_messages, projects_base
  to sdk/transcript.py re-export for complete public API surface
@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 33a7b83 — changed to len(source) so the logged count matches the source being compressed.

@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 33a7b83 — added explicit comment on the retry call explaining it intentionally uses full DB context rather than transcript+gap for the retry path.

majdyz added a commit that referenced this pull request Apr 16, 2026
…e %-logging

- _process_cli_restore: fix 'lines stripped' log metric — was reporting
  remaining line count; now correctly computes original_lines - remaining_lines
- _restore_cli_session_for_turn: narrow broad 'except Exception' to
  (UnicodeDecodeError, ValueError, OSError) so unexpected programming errors
  in strip_for_upload / validate_transcript are not silently masked
- _compress_messages: convert f-string logger.info to %-style lazy formatting
  to avoid unnecessary string interpolation when the log level is disabled
@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 review findings, addressed in add4f83

Bug fix: _process_cli_restore was logging remaining line count as "lines stripped" — the metric should be original lines minus remaining lines. Fixed by computing _original_lines - _remaining_lines.

Should Fix: _restore_cli_session_for_turn's bare except Exception in the baseline-download builder-load block was silently masking unexpected errors from strip_for_upload/validate_transcript. Narrowed to (UnicodeDecodeError, ValueError, OSError).

Nit: _compress_messages used f-string in logger.info — converted to %-style lazy formatting.

@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

E2E Test Report

Branch: fix/copilot-single-session-store
Date: 2026-04-16
Result: ALL PASS (20/20 — including both BLOCKER tests)


Standard Tests

Test Result Notes
S1: SDK cold start PASS mode=sdk in upload logs
S2: Baseline cold start PASS mode=baseline in upload logs
S3: SDK multi-turn --resume PASS T2 uses resume=True, keyword recalled
S4: SDK→Baseline→SDK (BLOCKER) PASS CROSSMODE_ALPHA_99 recalled in T3
S5: Baseline→SDK→Baseline (BLOCKER) PASS BASELINE_FIRST_55 recalled through full cycle

Invasive Tests

Test Result Notes
I1: Inject mode=baseline into SDK meta PASS SDK skipped --resume, used extract_context_messages
I2: Inject mode=garbage into meta PASS Graceful fallback to mode=sdk, no crash
I3: Corrupt JSONL (truncate mid-line) PASS Graceful DB fallback, no crash
I4: Delete JSONL, keep meta PASS Graceful fallback, fresh JSONL created
I5: Delete meta, keep JSONL PASS Defaults to mode=sdk, resume works
I6: Stale watermark PASS Gap-fill fires correctly (query_len increased from 6 to 147)
I7: Atomic upload rollback PASS storage.delete called on JSONL when meta write fails

Edge Cases

Test Result Notes
E1: Empty message PASS Graceful error: "Message cannot be empty."
E2: Very long message (10k+ chars) PASS No crash, response returned
E3: Unicode/emoji across mode switch PASS ROCKET_EMOJI_TEST recalled verbatim
E4: Service restart mid-session PASS Keyword recalled after container restart via GCS transcript
E5: Rapid back-to-back messages PASS msg_count: 2→4→6→8→10, no conflicts

Negative / Regression

Test Result Notes
N1: SDK --resume still works PASS resume=True on T2, keyword recalled
N2: Baseline multi-turn PASS Sequential uploads, correct msg_count
N3: Cold start — transcript after response PASS T1 upload logged after response

Unit Tests

  • transcript_test.py: 86/86 PASS
  • sdk/ test suite: 898/898 PASS (4 xfailed expected)
  • baseline/ test suite: 67/67 PASS

Key Behaviors Verified

  1. mode=sdk in meta → SDK uses --resume
  2. mode=baseline in meta → SDK skips --resume, uses extract_context_messages
  3. mode=garbage → gracefully defaults to sdk
  4. Atomic upload: JSONL rolled back when meta write fails
  5. Watermark fix prevents false gap detection after --resume
  6. extract_context_messages provides correct cross-mode context (compacted transcript + gap)

Log Evidence

[SDK][b4bc941b-73f][T1] Uploaded CLI session (3435B, msg_count=2, mode=sdk)
[Baseline] Uploaded CLI session (522B, msg_count=2, mode=baseline)
[SDK][18e7c922-708][T3] Transcript written by mode='baseline' — skipping --resume, will use transcript content + gap for context
[SDK][18e7c922-708][T3] Uploaded CLI session (3514B, msg_count=6, mode=sdk)

Note on Build

Initial test found stale container images (code without mode-gated --resume). Used docker compose build --no-cache to pick up PR changes.

Screenshots

00-login-already-in.png
01-copilot-page.png
02-copilot-sessions.png
03-s4-sdk-baseline-sdk.png

…ng tool_call_id

In both _session_messages_to_transcript (SDK path) and _append_gap_to_builder
(baseline path), silently skipping a tool message with no tool_call_id can
leave the TranscriptBuilder missing a tool_result entry, which would corrupt
the JSONL conversation tree used by --resume.  Replace the silent drop with an
explicit warning so the issue is visible in logs, making it easier to diagnose
data corruption rather than discovering it as a broken --resume session later.
@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Round 4 review findings, addressed in 8b9b2e0

Should Fix: Both _session_messages_to_transcript (SDK) and _append_gap_to_builder (baseline) silently dropped tool role messages with tool_call_id=None. A missing tool_result entry in the TranscriptBuilder produces a malformed JSONL conversation tree that breaks --resume. Changed to log a warning explicitly so data corruption surfaces in logs rather than manifesting as a broken resume session silently.

…ntent_blocks

The unknown block type warning was using an f-string which forces string
interpolation even when the log level is disabled. Convert to %-style lazy
formatting for consistency with the rest of the logging in the module.
@majdyz

majdyz commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Round 5 review findings, addressed in 44e070c

Nit: _format_sdk_content_blocks had an f-string in a logger.warning call. Converted to %-style lazy formatting for consistency with the rest of the module.

All 5 rounds complete. Full commit trail: b05846d (round 1), 33a7b83 (round 2), add4f83 (round 3), 8b9b2e0 (round 4), 44e070c (round 5).

majdyz added 3 commits April 16, 2026 14:57
_load_prior_transcript was returning (False, None) for missing/invalid
transcripts, preventing the upload guard from firing. The intent was to
protect against overwriting a *newer* GCS version — but a missing or
corrupt file has nothing worth protecting. Only download errors (unknown
GCS state) should suppress upload now.

Root cause of the session 7803bde1 bug: the baseline turn ran with 23
session messages, no transcript existed in GCS (first baseline turn in
that session), _load_prior_transcript returned (False, None), and
should_upload_transcript gated the upload to False. The SDK's subsequent
turn found no baseline JSONL and fell back to full DB reconstruction.

Also renames transcript_covers_prefix → transcript_upload_safe throughout
to accurately reflect the flag's semantics.
…n baseline

Accessing session.messages[-1] raised IndexError when a session had no
messages (e.g. tool-result submission with no message param). Changed to
conditional expression so an empty session safely produces an empty
messages_for_context list rather than crashing.
Comment thread autogpt_platform/backend/backend/copilot/sdk/service.py Outdated
@majdyz
majdyz merged commit 0d4b31e into master Apr 16, 2026
42 checks passed
@majdyz
majdyz deleted the fix/copilot-single-session-store branch April 16, 2026 08:35
@github-project-automation github-project-automation Bot moved this from 🆕 Needs initial review to ✅ Done in AutoGPT development kanban Apr 16, 2026
@github-project-automation github-project-automation Bot moved this to Done in Frontend Apr 16, 2026
majdyz added a commit that referenced this pull request Apr 16, 2026
Conflicts in baseline/service.py, baseline/transcript_integration_test.py,
and transcript.py arose because dev-only commit 0cd0a76
(baseline upload fix) overlapped with the same fix in PR #12804 which
landed in master. Took master's version for all three files — it is the
complete, reviewed implementation.
majdyz added a commit that referenced this pull request Apr 16, 2026
… Pyright warnings (#12812)

## Why
After PR #12804 was squashed into dev, two module-level helper functions
in `backend/copilot/sdk/service.py` remained private (`_`-prefixed)
while being directly imported by name in `sdk/transcript_test.py`.
Pyright reports `reportAttributeAccessIssue` when tests (even those
excluded from CI lint) import private symbols from outside their
defining module.

## What
Rename two helpers to remove the underscore prefix:
- `_process_cli_restore` → `process_cli_restore`
- `_read_cli_session_from_disk` → `read_cli_session_from_disk`

Update call sites in `service.py` and imports/calls/docstrings in
`sdk/transcript_test.py`.

## How
Pure rename — no logic change. Both functions were already module-level
helpers with no reason to be private; the underscore was convention
carried over during the refactor but they are directly unit-tested and
should be public.

All 66 `sdk/transcript_test.py` tests pass after the rename.

## Checklist
- [x] Tests pass (`poetry run pytest
backend/copilot/sdk/transcript_test.py`)
- [x] No `_`-prefixed symbols imported across module boundaries
- [x] No linter suppressors added
majdyz added a commit that referenced this pull request Apr 16, 2026
Conflicts in baseline/service.py, baseline/transcript_integration_test.py,
and transcript.py arose because dev-only commit 0cd0a76
(baseline upload fix) overlapped with the same fix in PR #12804 which
landed in master. Took master's version for all three files — it is the
complete, reviewed implementation.
Otto-AGPT pushed a commit that referenced this pull request Apr 16, 2026
…xt_messages, mode-gated --resume, compaction-aware gap-fill (#12804)

**Why:** The copilot had two separate GCS paths (`cli-sessions/` and
`chat-transcripts/`), redundant function names
(`upload_cli_session`/`restore_cli_session`), and no shared context
strategy between modes. When switching from baseline→SDK or
SDK→baseline, the receiving mode discarded the stored transcript and
fell back to full DB reconstruction — loading all raw messages instead
of the compacted form — causing inflated context, wasted tokens, and
loss of CLI compaction summaries.

**What:**
- Single GCS path (`cli-sessions/`) for both modes — `chat-transcripts/`
removed
- Unified public API: `upload_transcript` / `download_transcript` /
`TranscriptDownload`
- `TranscriptMode = Literal["sdk", "baseline"]` persisted in
`.meta.json` — SDK skips `--resume` when `mode != "sdk"`
(baseline-written JSONL has stripped fields / synthetic IDs)
- `extract_context_messages(download, session_messages)` — shared
context primitive used by **both SDK and baseline**: reads compacted
transcript content + fills only the DB gap (messages after watermark),
so CLI compaction summaries are preserved across mode switches
- Watermark fix: `_jsonl_covered = transcript_msg_count + 2` when a real
transcript is present, preventing false gap detection after `--resume`
- Baseline gap-fill: `_append_gap_to_builder` converts `ChatMessage` →
JSONL entries; no more silently discarded stale transcripts

**How:**

```
SDK turn (mode="sdk" transcript available):
  ──► --resume  [full CLI session restored natively]
  ──► inject gap prefix if DB has messages after watermark

SDK turn (mode="baseline" transcript available):
  ──► cannot --resume (synthetic CLI IDs)
  ──► extract_context_messages(download, session_messages):
        returns transcript JSONL (compacted, isCompactSummary preserved) + gap
        excludes session_messages[-1] (current turn — caller injects it separately)
  ──► format as <conversation_history> + "Now, the user says: {current}"

Baseline turn (any transcript):
  ──► _load_prior_transcript → TranscriptDownload
  ──► extract_context_messages(download, session_messages) + session_messages[-1]
        replaces full session.messages DB read
  ──► LLM messages: [compacted history + gap] + [current user turn]

Transcript unavailable — both SDK (use_resume=False) and baseline:
  ──► extract_context_messages(None, session_messages) returns session_messages[:-1]
        (all prior DB messages except the current user turn at [-1])
  ──► graceful fallback — no crash, no empty context
  ──► covers: first turn, GCS error, corrupt JSONL, missing .meta.json
  ──► next successful response uploads a fresh transcript
```

`extract_context_messages` is the shared primitive — both modes call the
same function, which handles:
- `download=None` (first turn, GCS unavailable) → falls back to
`session_messages[:-1]`
- Empty/corrupt content → falls back to `session_messages[:-1]`
- `bytes` content (raw GCS) or `str` content (pre-decoded baseline path)
- `isCompactSummary=True` entries → preserved so CLI compaction survives
mode switches
- Missing/corrupt `.meta.json` → `message_count` defaults to `0`, `mode`
defaults to `"sdk"`

**Why `[:-1]` and not all messages?** `session_messages[-1]` is always
the current user turn being handled right now. Both callers inject it
separately — SDK wraps it as `"Now, the user says: ..."`, baseline
appends it as the final message in the LLM array. Returning it inside
`extract_context_messages` would double-inject it.

- **`transcript.py`**: `CliSessionRestore` → `TranscriptDownload` +
`mode` field; `upload_cli_session` → `upload_transcript`;
`restore_cli_session` → `download_transcript`; add `TranscriptMode`,
`detect_gap`, `extract_context_messages`; import `ChatMessage` via
relative path to match `service.py` style
- **`sdk/service.py`**: mode-check before `--resume`; `_RestoreResult`
carries `baseline_download` + `context_messages` + `transcript_content`;
`_build_query_message` accepts `prior_messages` override;
`_restore_cli_session_for_turn` populates `context_messages` via
`extract_context_messages` and sets `transcript_content` to prevent
duplicate DB reconstruction; watermark fix (`_jsonl_covered =
transcript_msg_count + 2`)
- **`baseline/service.py`**: `_load_prior_transcript` returns `(bool,
TranscriptDownload | None)`; LLM context replaced with
`extract_context_messages(download, messages)`; `_append_gap_to_builder`
+ `detect_gap` call; `upload_transcript(mode="baseline")`
- **`sdk/transcript.py`**: updated re-exports, old aliases removed
- **`scripts/download_transcripts.py`**: updated for `bytes | str`
content type
- **Test files**: 179 tests total; `transcript_test.py`,
`baseline/transcript_integration_test.py`,
`sdk/service_helpers_test.py`, `sdk/test_transcript_watermark.py`,
`test/copilot/test_transcript_watermark.py` all updated/added

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] 179 unit tests pass — `transcript_test`,
`baseline/transcript_integration_test`, `sdk/service_helpers_test`,
`sdk/test_transcript_watermark`
  - [x] pyright 0 errors on all changed files
- [x] SDK `--resume` path still works when `mode="sdk"` transcript is
present
- [x] SDK fallback uses `extract_context_messages` (compacted baseline
content + gap) when `mode="baseline"` transcript is stored — no more
full DB reconstruction
- [x] Baseline uses `extract_context_messages` per turn instead of full
`session.messages` DB read
  - [x] `isCompactSummary=True` entries preserved across mode switches
- [x] Watermark (`_jsonl_covered`) fix prevents false gap detection
after `--resume`
- [x] Baseline gap detection no longer silently discards stale
transcripts
- [x] `TranscriptDownload.content` accepts `bytes | str` — backward
compatible
- [x] Transcript unavailable (GCS error, first turn, corrupt file)
gracefully falls back to `session_messages[:-1]` without crash — applies
to both SDK and baseline paths

---------

Co-authored-by: chernistry <73943355+chernistry@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Otto-AGPT pushed a commit that referenced this pull request Apr 16, 2026
… Pyright warnings (#12812)

## Why
After PR #12804 was squashed into dev, two module-level helper functions
in `backend/copilot/sdk/service.py` remained private (`_`-prefixed)
while being directly imported by name in `sdk/transcript_test.py`.
Pyright reports `reportAttributeAccessIssue` when tests (even those
excluded from CI lint) import private symbols from outside their
defining module.

## What
Rename two helpers to remove the underscore prefix:
- `_process_cli_restore` → `process_cli_restore`
- `_read_cli_session_from_disk` → `read_cli_session_from_disk`

Update call sites in `service.py` and imports/calls/docstrings in
`sdk/transcript_test.py`.

## How
Pure rename — no logic change. Both functions were already module-level
helpers with no reason to be private; the underscore was convention
carried over during the refactor but they are directly unit-tested and
should be public.

All 66 `sdk/transcript_test.py` tests pass after the rename.

## Checklist
- [x] Tests pass (`poetry run pytest
backend/copilot/sdk/transcript_test.py`)
- [x] No `_`-prefixed symbols imported across module boundaries
- [x] No linter suppressors added
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/xl

Projects

Status: ✅ Done
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants