mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## 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:
parent
1c947b1103
commit
3e85eb1880
2 changed files with 82 additions and 0 deletions
|
|
@ -448,4 +448,25 @@ def extract_system_prompt(body: Mapping[str, Any]) -> str:
|
||||||
if parts:
|
if parts:
|
||||||
return "\n".join(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 ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,67 @@ def test_extract_system_prompt_missing_returns_empty() -> None:
|
||||||
assert extract_system_prompt({"messages": []}) == ""
|
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).
|
# BackendRouter path-layout tests (no real backend I/O — we stub the class).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue