Fix workspace prefix sentinel handling
This commit is contained in:
committed by
nesquena-hermes
parent
6253032b53
commit
ccdc055c36
@@ -586,9 +586,25 @@ def _message_text(value) -> str:
|
||||
return _strip_thinking_markup(str(value or '').strip())
|
||||
|
||||
|
||||
def _strip_workspace_prefix(text: str) -> str:
|
||||
"""Remove WebUI's model-facing workspace tag from display identity text."""
|
||||
return re.sub(r'^\s*\[Workspace:[^\]]+\]\s*', '', str(text or '')).strip()
|
||||
_WORKSPACE_PREFIX_RE = re.compile(r'^\s*\[Workspace::v1:\s*(?:\\.|[^\]\\])+\]\s*')
|
||||
_LEGACY_WORKSPACE_PREFIX_RE = re.compile(r'^\s*\[Workspace:[^\]]+\]\s*')
|
||||
|
||||
|
||||
def _escape_workspace_prefix_path(path: str) -> str:
|
||||
return str(path or '').replace('\\', '\\\\').replace(']', '\\]')
|
||||
|
||||
|
||||
def _workspace_context_prefix(path: str) -> str:
|
||||
return f"[Workspace::v1: {_escape_workspace_prefix_path(path)}]\n"
|
||||
|
||||
|
||||
def _strip_workspace_prefix(text: str, *, include_legacy: bool = False) -> str:
|
||||
"""Remove WebUI-injected workspace tags without eating user-typed text."""
|
||||
value = str(text or '')
|
||||
stripped = _WORKSPACE_PREFIX_RE.sub('', value, count=1)
|
||||
if include_legacy and stripped == value:
|
||||
stripped = _LEGACY_WORKSPACE_PREFIX_RE.sub('', value, count=1)
|
||||
return stripped.strip()
|
||||
|
||||
|
||||
def _first_exchange_snippets(messages):
|
||||
@@ -1051,7 +1067,7 @@ def _fallback_title_from_exchange(user_text: str, assistant_text: str) -> Option
|
||||
assistant_text = _strip_thinking_markup(assistant_text or '').strip()
|
||||
if not user_text:
|
||||
return None
|
||||
user_text = re.sub(r'^\[Workspace:[^\]]+\]\s*', '', user_text)
|
||||
user_text = _strip_workspace_prefix(user_text)
|
||||
user_text = re.sub(r'\s+', ' ', user_text).strip()
|
||||
assistant_text = re.sub(r'\s+', ' ', assistant_text).strip()
|
||||
combined = f"{user_text} {assistant_text}".strip().lower()
|
||||
@@ -1443,7 +1459,7 @@ def _message_identity(msg):
|
||||
# visible optimistic bubble contains only the human text. Treat them as
|
||||
# the same turn for merge/dedup purposes; otherwise compaction results
|
||||
# render two adjacent user bubbles ("Ok" and "[Workspace...]\nOk").
|
||||
text = _strip_workspace_prefix(text)
|
||||
text = _strip_workspace_prefix(text, include_legacy=True)
|
||||
if not text and not msg.get('tool_call_id') and not msg.get('tool_calls'):
|
||||
return None
|
||||
return (
|
||||
@@ -1482,7 +1498,12 @@ def _find_current_user_turn(messages, msg_text):
|
||||
if not isinstance(msg, dict) or msg.get('role') != 'user':
|
||||
continue
|
||||
fallback = idx
|
||||
text = " ".join(_strip_workspace_prefix(_message_text(msg.get('content', ''))).split())
|
||||
text = " ".join(
|
||||
_strip_workspace_prefix(
|
||||
_message_text(msg.get('content', '')),
|
||||
include_legacy=True,
|
||||
).split()
|
||||
)
|
||||
if needle and (needle in text or text in needle):
|
||||
return idx
|
||||
return fallback
|
||||
@@ -2534,15 +2555,15 @@ def _run_agent_streaming(
|
||||
|
||||
# Prepend workspace context so the agent always knows which directory
|
||||
# to use for file operations, regardless of session age or AGENTS.md defaults.
|
||||
workspace_ctx = f"[Workspace: {s.workspace}]\n"
|
||||
workspace_ctx = _workspace_context_prefix(str(s.workspace))
|
||||
workspace_system_msg = (
|
||||
f"Active workspace at session start: {s.workspace}\n"
|
||||
"Every user message is prefixed with [Workspace: /absolute/path] indicating the "
|
||||
"Every user message is prefixed with [Workspace::v1: /absolute/path] indicating the "
|
||||
"workspace the user has selected in the web UI at the time they sent that message. "
|
||||
"This tag is the single authoritative source of the active workspace and updates "
|
||||
"with every message. It overrides any prior workspace mentioned in this system "
|
||||
"prompt, memory, or conversation history. Always use the value from the most recent "
|
||||
"[Workspace: ...] tag as your default working directory for ALL file operations: "
|
||||
"[Workspace::v1: ...] tag as your default working directory for ALL file operations: "
|
||||
"write_file, read_file, search_files, terminal workdir, and patch. "
|
||||
"Never fall back to a hardcoded path when this tag is present."
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ function _isBacktickFenceClose(line,minLen){
|
||||
*/
|
||||
|
||||
function _stripWorkspaceDisplayPrefix(text){
|
||||
return String(text||'').replace(/^\s*\[Workspace:[^\]]+\]\s*/,'').trim();
|
||||
return String(text||'').replace(/^\s*\[Workspace::v1:\s*(?:\\.|[^\]\\])+\]\s*/,'').trim();
|
||||
}
|
||||
function _renderUserFencedBlocks(text){
|
||||
const stash=[];
|
||||
|
||||
34
tests/test_issue1913_workspace_prefix_sentinel.py
Normal file
34
tests/test_issue1913_workspace_prefix_sentinel.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from api.streaming import (
|
||||
_fallback_title_from_exchange,
|
||||
_strip_workspace_prefix,
|
||||
_workspace_context_prefix,
|
||||
)
|
||||
|
||||
|
||||
def test_workspace_prefix_strips_only_versioned_sentinel():
|
||||
assert _strip_workspace_prefix("[Workspace::v1: /tmp/project]\nHello") == "Hello"
|
||||
assert _strip_workspace_prefix("[Workspace: /tmp/project]\nHello") == "[Workspace: /tmp/project]\nHello"
|
||||
|
||||
|
||||
def test_workspace_prefix_escapes_paths_with_closing_brackets():
|
||||
prefix = _workspace_context_prefix("/tmp/proj-[wip]/src")
|
||||
|
||||
assert prefix == "[Workspace::v1: /tmp/proj-[wip\\]/src]\n"
|
||||
assert _strip_workspace_prefix(f"{prefix}Continue") == "Continue"
|
||||
|
||||
|
||||
def test_legacy_workspace_prefix_only_strips_for_compatibility_callers():
|
||||
legacy = "[Workspace: /tmp/project]\nContinue"
|
||||
|
||||
assert _strip_workspace_prefix(legacy) == legacy
|
||||
assert _strip_workspace_prefix(legacy, include_legacy=True) == "Continue"
|
||||
|
||||
|
||||
def test_user_typed_legacy_workspace_prefix_survives_fallback_title():
|
||||
title = _fallback_title_from_exchange(
|
||||
"[Workspace: /tmp/project]\nExplain this literal prefix",
|
||||
"Sure",
|
||||
)
|
||||
|
||||
assert title is not None
|
||||
assert title.startswith("Workspace tmp/project")
|
||||
@@ -16,7 +16,8 @@ def test_workspace_display_prefix_helper_strips_leading_metadata_only():
|
||||
assert end != -1, "user fenced block renderer not found after prefix stripper"
|
||||
helper = src[start:end]
|
||||
|
||||
assert r"^\s*\[Workspace:[^\]]+\]\s*" in helper
|
||||
assert r"^\s*\[Workspace::v1:\s*(?:\\.|[^\]\\])+\]\s*" in helper
|
||||
assert "[Workspace:[^\\]]+" not in helper
|
||||
assert ".trim()" in helper
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user