fix(plugin): the session-start Goal line cuts at a word and says where the rest is (#4036)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Successful in 26s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Successful in 26s
A raw 200-char slice ended mid-word with nothing marking the cut, so a reader took half a sentence for the whole goal. _goal_line flattens the goal to one line, trims at a word break with an ellipsis, and points at enter_project(id) when it cut. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ _MAX_CHARS = 9000
|
|||||||
# Max chars of a Process body to fold into the auto-surface description.
|
# Max chars of a Process body to fold into the auto-surface description.
|
||||||
_PROC_PREVIEW_CHARS = 200
|
_PROC_PREVIEW_CHARS = 200
|
||||||
|
|
||||||
|
# Max chars of the project goal on the session-start Goal line. The full goal is
|
||||||
|
# one enter_project away; a cut says so rather than ending mid-word (#4036).
|
||||||
|
_GOAL_CHARS = 200
|
||||||
|
|
||||||
# --- Knowledge auto-inject (Path A: per-turn awareness push) -----------------
|
# --- Knowledge auto-inject (Path A: per-turn awareness push) -----------------
|
||||||
# Per-user settings (keys live in the generic settings table). The threshold is
|
# Per-user settings (keys live in the generic settings table). The threshold is
|
||||||
# deliberately STRICTER than the pull-search default (embeddings
|
# deliberately STRICTER than the pull-search default (embeddings
|
||||||
@@ -2140,6 +2145,23 @@ def _stamp_line(path: str, stamped: list[dict]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _goal_line(goal: str, project_id: int) -> str:
|
||||||
|
"""The Goal line, trimmed at a word break with a visible cut.
|
||||||
|
|
||||||
|
A raw slice ended mid-word with nothing to say more existed, so a reader
|
||||||
|
took half a sentence for the whole goal (#4036).
|
||||||
|
"""
|
||||||
|
if not goal:
|
||||||
|
return ""
|
||||||
|
flat = " ".join(goal.split())
|
||||||
|
if len(flat) <= _GOAL_CHARS:
|
||||||
|
return f"Goal: {flat}"
|
||||||
|
short = textwrap.shorten(flat, width=_GOAL_CHARS, placeholder="…")
|
||||||
|
if short == "…": # one unbroken word longer than the cap
|
||||||
|
short = flat[: _GOAL_CHARS - 1] + "…"
|
||||||
|
return f"Goal: {short} (full goal: `enter_project({project_id})`)"
|
||||||
|
|
||||||
|
|
||||||
async def build_session_context(
|
async def build_session_context(
|
||||||
user_id: int, project_id: int = 0, unbound_repo: str = ""
|
user_id: int, project_id: int = 0, unbound_repo: str = ""
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -2185,7 +2207,7 @@ async def build_session_context(
|
|||||||
lines += [
|
lines += [
|
||||||
"",
|
"",
|
||||||
f"## Active project: {project.title} (id {project.id})",
|
f"## Active project: {project.title} (id {project.id})",
|
||||||
f"Goal: {goal[:200]}" if goal else "",
|
_goal_line(goal, project.id),
|
||||||
f"Open todo tasks: {open_count}",
|
f"Open todo tasks: {open_count}",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,32 @@ async def test_build_session_context_includes_project_when_scoped():
|
|||||||
assert "Reflex:" not in out["context"]
|
assert "Reflex:" not in out["context"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_short_goal_is_shown_whole():
|
||||||
|
from scribe.services.plugin_context import _goal_line
|
||||||
|
assert _goal_line("ship it", 2) == "Goal: ship it"
|
||||||
|
assert _goal_line("", 2) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_long_goal_is_cut_at_a_word_and_says_where_the_rest_is():
|
||||||
|
"""#4036: a raw slice ended mid-word with nothing marking the cut, so a
|
||||||
|
reader took half a sentence for the whole goal."""
|
||||||
|
from scribe.services.plugin_context import _GOAL_CHARS, _goal_line
|
||||||
|
goal = "make the record reach the next session\nso a solution is recalled " * 10
|
||||||
|
line = _goal_line(goal, 2)
|
||||||
|
shown = line.removeprefix("Goal: ").split(" (full goal:")[0]
|
||||||
|
assert shown.endswith("…") and len(shown) <= _GOAL_CHARS
|
||||||
|
# The cut lands between words: what precedes the ellipsis is a whole word.
|
||||||
|
assert shown[:-1].rstrip().split()[-1] in goal.split()
|
||||||
|
assert "\n" not in line
|
||||||
|
assert line.endswith("(full goal: `enter_project(2)`)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unbroken_goal_still_shows_its_start():
|
||||||
|
from scribe.services.plugin_context import _GOAL_CHARS, _goal_line
|
||||||
|
shown = _goal_line("x" * 500, 2).removeprefix("Goal: ").split(" (full goal:")[0]
|
||||||
|
assert shown == "x" * (_GOAL_CHARS - 1) + "…"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_build_session_context_pushes_the_projects_design_system():
|
async def test_build_session_context_pushes_the_projects_design_system():
|
||||||
"""The gap this closes: a design system had no push channel, so its
|
"""The gap this closes: a design system had no push channel, so its
|
||||||
|
|||||||
Reference in New Issue
Block a user