fix(memory): resolve Trae cwd metadata from user reminders (#1737) (#1887)
Some checks failed
CI / changes (push) Failing after 40s
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 16s
Dev Containers / validate-worktree (push) Failing after 15s
CI / commitlint (push) Failing after 4s
CI / lint (push) Failing after 6s
CI / build-wheel (push) Failing after 5s
CI / prefetch-model (push) Failing after 4s
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
CI / build (push) Failing after 4s
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Waiting to run
CI / test-extras (push) Failing after 2s
CI / workflow-validation (push) Failing after 5s
CI / test-agno (push) Failing after 6s
CI / docker-native-e2e (push) Failing after 5s
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Blocked by required conditions
CI / test (2) (push) Failing after 5s
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Blocked by required conditions
CI / test-dashboard-ui (push) Failing after 5s
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Blocked by required conditions
Docker / promote-latest (push) Blocked by required conditions
Init E2E / docker-init-e2e (push) Failing after 5s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 5s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 6s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 7s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 7s
Merge Conflicts / merge-conflicts (push) Failing after 6s
Release Please / release-please (push) Failing after 7s
Security / Secret scan (gitleaks) (push) Failing after 47s
Security / CodeQL (python) (push) Failing after 48s
Wrap E2E / docker-wrap-e2e (push) Failing after 32s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 30s
Security / Dependency audit (pip-audit) (push) Failing after 1m41s
Security / CodeQL (javascript-typescript) (push) Failing after 12m0s
CI / test (4) (push) Failing after 12m57s
CI / test (3) (push) Failing after 13m0s
CI / test (1) (push) Failing after 13m7s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 14m22s
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled

## Description

Project memory routing misses Trae Desktop workspaces when Trae sends
the cwd inside a user-message `<system-reminder>` block. The existing
resolver already understands `cwd:` once the text reaches
`ProjectResolver`, but `extract_system_prompt()` only reads top-level
system fields and `role == "system"` messages, so the Trae metadata is
dropped before routing can use it. This adds a narrow fallback that
scans user-message text only when no system prompt was found and only
returns that text when it contains one of the existing cwd prefixes.
Closes #1737.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Extended `extract_system_prompt()` with a cwd-prefix-gated
user-message fallback for OpenAI-compatible payloads that carry
environment metadata in text blocks.
- Kept top-level `system` and `role == "system"` precedence unchanged,
so regular system prompt routing still wins over user fallback content.
- Added focused storage-router tests for the Trae `<system-reminder>`
payload shape, ordinary user text without cwd, system-message
precedence, and a non-user cwd spoof boundary.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_memory_storage_router.py -v`)
- [x] Linting passes (`uv run ruff check
headroom/memory/storage_router.py tests/test_memory_storage_router.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_memory_storage_router.py -v
26 passed in 0.23s

uv run ruff check headroom/memory/storage_router.py tests/test_memory_storage_router.py
All checks passed!

uv run ruff format headroom/memory/storage_router.py tests/test_memory_storage_router.py --check
2 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python via `uv`, no live Trae client required
for the unit-level payload regression.
- Exact command / steps: run the focused storage-router pytest against a
request body shaped like the issue's Trae payload, with
`messages[0].role == "user"` and a text block containing
`<system-reminder>` plus `cwd:
S:\workspace-zhuangxiu\decorate-offer-api`.
- Observed result: the extracted prompt reaches `ProjectResolver`, and
the resolved display name is `decorate-offer-api`; ordinary user text
without cwd still returns an empty prompt; an explicit system message
still wins over a user cwd fallback.
- Not tested: live Trae Desktop network capture and full-suite CI, which
remain outside this focused routing fix.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

`CHANGELOG.md` is unchanged because this repo generates release notes
from conventional commits. Type checking is not part of the focused
local proof for this Python-only storage-router change. The fix is
intentionally scoped to request prompt extraction and does not add
Trae-specific branches to OpenAI handlers.
This commit is contained in:
Rod Boev 2026-07-08 18:49:21 -04:00 committed by GitHub
parent 1c947b1103
commit 3e85eb1880
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 0 deletions

View file

@ -448,4 +448,25 @@ def extract_system_prompt(body: Mapping[str, Any]) -> str:
if parts:
return "\n".join(parts)
for msg in messages:
if not isinstance(msg, dict):
continue
if msg.get("role") != "user":
continue
content = msg.get("content")
user_text: str | None = None
if isinstance(content, str):
user_text = content
elif isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict):
text = block.get("text")
if isinstance(text, str):
parts.append(text)
if parts:
user_text = "\n".join(parts)
if user_text and any(prefix in user_text for prefix in _CWD_PREFIXES):
return user_text
return ""

View file

@ -155,6 +155,67 @@ def test_extract_system_prompt_missing_returns_empty() -> None:
assert extract_system_prompt({"messages": []}) == ""
def test_extract_system_prompt_user_reminder_with_cwd_reaches_resolver() -> None:
body = {
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": (
"<system-reminder>\n\n"
"The maximum number of terminals is 5.\n\n"
"<available_terminal>\n"
"- terminal_id: 9\n"
"- cwd: S:\\workspace-zhuangxiu\\decorate-offer-api\n"
"</available_terminal>\n\n"
"</system-reminder>"
),
}
],
}
]
}
prompt = extract_system_prompt(body)
assert "cwd:" in prompt
resolved = ProjectResolver().resolve(_ctx(system_prompt=prompt))
assert resolved is not None
_, display = resolved
assert "decorate-offer-api" in display
def test_extract_system_prompt_ordinary_user_text_returns_empty() -> None:
body = {"messages": [{"role": "user", "content": "Hello, can you help me refactor this?"}]}
assert extract_system_prompt(body) == ""
def test_extract_system_prompt_system_message_beats_user_cwd_fallback() -> None:
body = {
"messages": [
{"role": "system", "content": "Working directory: /system/project"},
{"role": "user", "content": "cwd: /user/project\nDo the thing."},
]
}
prompt = extract_system_prompt(body)
resolved = ProjectResolver().resolve(_ctx(system_prompt=prompt))
assert prompt == "Working directory: /system/project"
assert resolved is not None
_, display = resolved
assert display == "project"
def test_extract_system_prompt_cwd_in_non_user_message_returns_empty() -> None:
body = {"messages": [{"role": "assistant", "content": "cwd: /spoof/project"}]}
assert extract_system_prompt(body) == ""
# ---------------------------------------------------------------------------
# BackendRouter path-layout tests (no real backend I/O — we stub the class).
# ---------------------------------------------------------------------------