From d97592c5c634b198d2971db390b119fd3663d266 Mon Sep 17 00:00:00 2001 From: Ansu <230804140223@stu.nepu.edu.cn> Date: Sun, 18 Oct 2026 11:22:11 +0800 Subject: [PATCH 1/2] fix(sleep): harvest pasted briefs instead of dropping them as host noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user message that arrives wrapped in angle brackets was classified as system noise, so a brief pasted into the Claude Code desktop app (`…`) never became a task: the session was mined from a short follow-up instead, or dropped entirely when the brief was its only user turn. - unwrap `` and keep the inner text as the user's - drop host-injected `` blocks from the intent rather than copying them into it - keep the "message is nothing but one host tag block" filter, but only for a closed list of host tags (command / local-command / system-reminder / bash / IDE / Copilot `environment_info`), so a wrapper this list does not know about loses its tags rather than the session's request - treat a session whose user turns were all filtered as engine-generated only when it has no assistant work Fixes #318 --- skillopt_sleep/harvest.py | 69 +++++++++++++++++++-- tests/test_sleep_engine.py | 122 +++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 5 deletions(-) diff --git a/skillopt_sleep/harvest.py b/skillopt_sleep/harvest.py index 7f3bee8f7..8ecf6e2e4 100644 --- a/skillopt_sleep/harvest.py +++ b/skillopt_sleep/harvest.py @@ -17,6 +17,7 @@ import json import os +import re from datetime import datetime from typing import Any, Dict, Iterable, List, Optional @@ -134,13 +135,56 @@ def _detect_feedback(text: str) -> List[str]: return sig +# ── Issue #318: host wrappers around user text ───────────────────────── +# +# The Claude Code desktop app delivers a pasted block as the user message, +# wrapped in ``…``. The brief +# the user pasted is typed by them and is often the whole point of the +# session, so the wrapper is unwrapped instead of dropped. +_PASTED_CONTENT_RE = re.compile(r"]*>", re.IGNORECASE) + +# Host-injected status notes and environment blocks. A message that is only +# a reminder is host noise, and a reminder prefixed to a real request must +# not ride into the mined intent as text the user never wrote. +_SYSTEM_REMINDER_RE = re.compile( + r".*?\s*", re.IGNORECASE | re.DOTALL +) + +# Tags the host injects as a whole user message: expanded slash-command +# bodies, local-command output, shell-mode input and editor selections. The +# list is closed deliberately — a wrapper this list does not know about is +# treated as user text, so such a message keeps its request instead of losing +# it. is listed as well as stripped, so a variant carrying +# attributes is still recognised as host noise. +_HOST_META_TAGS = ( + "command-name", "command-message", "command-args", + "local-command-stdout", "local-command-caveat", + "system-reminder", + "bash-input", "bash-stdout", "bash-stderr", + "ide_opened_file", "ide_selection", + # VS Code Copilot Chat brackets the machine context of every request + # (…); harvesting it leaks local + # environment detail and was never a user prompt. + "environment_info", +) + + +def _strip_host_wrappers(text: str) -> str: + """Unwrap pasted user content and drop host-injected blocks. + + Only the two wrappers above are touched: a message keeps its text when it + is wrapped in a tag the host does not own. + """ + if "<" not in text: + return text + return _PASTED_CONTENT_RE.sub("", _SYSTEM_REMINDER_RE.sub("", text)) + + def _is_meta_prompt(text: str) -> bool: """Skip slash-commands / system noise that aren't real user intents.""" - t = text.strip() + t = _strip_host_wrappers(text).strip() if not t: return True - if t.startswith("<") and t.endswith(">"): - return True if t.startswith("/") and len(t.split()) <= 3: return True if t.startswith("[Pasted text") or t.startswith("Caveat:"): @@ -152,6 +196,14 @@ def _is_meta_prompt(text: str) -> bool: return True if t.startswith("# /"): return True + # A message that is nothing but one host tag block (``, + # ``, …) carries no user intent. The tag list is + # closed on purpose: text that merely arrives wrapped in angle brackets + # is user content, and dropping it loses the session's real request + # (issue #318: the desktop app delivers a pasted brief that way). + if t.startswith("<") and t.endswith(">"): + head = t[1:].split(">", 1)[0].split(None, 1) + return bool(head) and head[0].lower() in _HOST_META_TAGS return False @@ -215,7 +267,11 @@ def _is_headless_replay(digest: "SessionDigest") -> bool: if digest.n_user_turns > 1: return False if digest.n_user_turns == 0: - return True + # Every user turn was filtered as host noise or engine input. That is + # not a replay signal by itself: a session whose user turns were all + # host-wrapped still holds real assistant work (#318), so only a + # transcript with no work at all counts as engine-generated. + return digest.n_assistant_turns == 0 prompt = digest.user_prompts[0] if digest.user_prompts else "" for marker in _REPLAY_PROMPT_MARKERS: if marker in prompt: @@ -278,7 +334,10 @@ def digest_transcript(path: str) -> Optional[SessionDigest]: # other sessions, usage-limit notices. The user typed none of it. # A body that names a known agent session is still kept, so # _is_agent_session can drop the whole session as before. - text = _text_from_content(content) + # Host wrappers are removed before the meta test: a pasted brief + # (#318) is user content, and a prefix must not + # ride into the intent (see _strip_host_wrappers). + text = _strip_host_wrappers(_text_from_content(content)) if rec.get("isMeta") is True and not any( marker in text for marker in _AGENT_SESSION_MARKERS ): diff --git a/tests/test_sleep_engine.py b/tests/test_sleep_engine.py index 62fe982e7..91662ba52 100644 --- a/tests/test_sleep_engine.py +++ b/tests/test_sleep_engine.py @@ -82,6 +82,128 @@ def test_meta_prompt_filter(self): self.assertTrue(_is_meta_prompt("x")) self.assertFalse(_is_meta_prompt("please refactor the auth module")) + def _write_claude_session(self, path, turns): + """Write one Claude Code transcript; ``turns`` are (role, text) pairs. + + Records are an hour apart so the sub-3-second heuristic in + _is_headless_replay never decides these fixtures. + """ + self._write_jsonl(path, [ + {"type": role, + "timestamp": f"2026-10-15T0{index + 1}:00:00Z", + "cwd": "/repo/example", + "sessionId": os.path.splitext(os.path.basename(path))[0], + "message": {"role": role, "content": text}} + for index, (role, text) in enumerate(turns) + ]) + + def test_meta_prompt_keeps_pasted_user_content(self): + # issue #318: the desktop app delivers a pasted brief inside a tag + # pair carrying the same id. The user typed that brief, so it is user + # content, and dropping it left the session's follow-up as the intent. + from skillopt_sleep.harvest import _strip_host_wrappers + + brief = ('\n' + 'Install tool X into a dedicated folder, pin the version.\n' + '') + + self.assertFalse(_is_meta_prompt(brief)) + # the helper removes the tags only; callers strip the remainder + self.assertEqual( + _strip_host_wrappers(brief), + "\nInstall tool X into a dedicated folder, pin the version.\n", + ) + + def test_meta_prompt_filters_only_known_host_tags(self): + self.assertTrue(_is_meta_prompt("ok")) + self.assertTrue(_is_meta_prompt( + "skillopt-sleep\n" + "/skillopt-sleep" + )) + # VS Code Copilot Chat brackets each request's machine context the + # same way; it is host detail, not a prompt, and must stay out + # (tests/test_copilot_harvest.py pins the privacy boundary). + self.assertTrue(_is_meta_prompt( + "private machine context" + )) + # A wrapper the host does not own is user text, not noise: it must + # survive the filter with its content instead of being dropped for + # being wrapped in angle brackets, which is what lost pasted briefs. + self.assertFalse(_is_meta_prompt("Ship 1.4")) + + def test_pasted_brief_becomes_the_mined_intent(self): + brief = ('\n' + 'Install tool X into a dedicated folder, pin the version, ' + 'back up the configs first.\n' + '') + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "pasted-session.jsonl") + self._write_claude_session(path, [ + ("user", brief), + ("assistant", "Plan: ..."), + ("user", "Yes, go ahead with option A."), + ("assistant", "Done."), + ]) + digest = digest_transcript(path) + + [task] = heuristic_mine([digest]) + self.assertEqual( + task.intent, + "Install tool X into a dedicated folder, pin the version, back up the configs first.", + ) + self.assertNotIn("pasted_content", task.intent) + # the follow-up stays available as constraint context, not as intent + self.assertIn("Yes, go ahead with option A.", task.context_excerpt) + + def test_session_whose_only_user_turn_is_a_pasted_brief_survives_harvest(self): + from skillopt_sleep.harvest import harvest + + brief = ('\nInstall tool X into a dedicated folder.\n' + '') + with tempfile.TemporaryDirectory() as tmp: + project_dir = os.path.join(tmp, "projects", "-repo-example") + os.makedirs(project_dir) + self._write_claude_session(os.path.join(project_dir, "pasted-only.jsonl"), [ + ("user", brief), + ("assistant", "Plan: ..."), + ("assistant", "Done."), + ]) + + digests = harvest(os.path.join(tmp, "projects"), scope="all") + + self.assertEqual([d.session_id for d in digests], ["pasted-only"]) + self.assertEqual(digests[0].user_prompts, + ["Install tool X into a dedicated folder."]) + + def test_system_reminder_prefix_does_not_ride_into_the_intent(self): + request = "Yes: run the third review on range r2..abc1234 and update the report." + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "reminder-prefix.jsonl") + self._write_claude_session(path, [ + ("user", "\nStatus note injected by the host app.\n" + f"\n\n{request}"), + ("assistant", "Running the review."), + ]) + digest = digest_transcript(path) + + self.assertEqual(digest.user_prompts, [request]) + [task] = heuristic_mine([digest]) + self.assertEqual(task.intent, request) + self.assertNotIn("Status note", task.intent) + + def test_session_without_user_turns_is_a_replay_only_when_it_has_no_work(self): + from skillopt_sleep.harvest import _is_headless_replay + + # issue #318: all user turns filtered out is not a replay signal on + # its own — real assistant work means the session is a real one. + worked = SessionDigest(session_id="s", project="/repo/example", + assistant_finals=["Done."], + n_user_turns=0, n_assistant_turns=2) + empty = SessionDigest(session_id="s", project="/repo/example") + + self.assertFalse(_is_headless_replay(worked)) + self.assertTrue(_is_headless_replay(empty)) + def _claude_session_with_injected_messages(self, tmp, follow_up): """A Claude Code session as written to disk when a skill is loaded. From f5f1f9a15510087da199b06cf294e899e8bc0cb2 Mon Sep 17 00:00:00 2001 From: Ansu <230804140223@stu.nepu.edu.cn> Date: Mon, 19 Oct 2026 13:06:24 +0800 Subject: [PATCH 2/2] fix(sleep): strip only the host's leading notes, keep no-intent records out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two boundary corrections from review. * Host stripping no longer rewrites text the user wrote. `_strip_host_wrappers` removed every `…` block in the message, so a prompt that quoted one — inline code, a fenced XML example — lost it, and both the harvested request and its mined intent changed. Removal is now anchored: the block is dropped only while it leads the message, which is the form the host prepends. A quoted sample anywhere after the user's own text survives verbatim. The tag match now also tolerates attributes. * A transcript with no user turns is a replay again, whatever assistant work it holds. The zero-user/assistant-present exception admitted host-only isMeta records before the session cap; such a record mines nothing, so under `--max-sessions` it displaced a real session and left the night with no tasks. Reverted to the base behaviour. The pasted-brief fix does not need the exception: unwrapping now yields a user turn, which is what the new tests assert. Measured with the new tests against the pre-review head `d97592c`: 3 failed, 136 passed, 1 skipped — test_inline_code_system_reminder_example_is_user_text ['How ... escape `` inside my transcript fixture?'] != [the prompt] test_fenced_system_reminder_example_is_user_text the reminder block was deleted from inside the fence test_host_only_transcript_does_not_take_a_session_slot ['host-note'] != ['real-task'] (limit=1, host-only record newer) After: 139 passed, 1 skipped, 12 subtests. Full suite: 1,809 passed, 11 skipped, 359 subtests. --- skillopt_sleep/harvest.py | 41 ++++++++++++++-------- tests/test_sleep_engine.py | 72 +++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/skillopt_sleep/harvest.py b/skillopt_sleep/harvest.py index 8ecf6e2e4..da0ac9dc4 100644 --- a/skillopt_sleep/harvest.py +++ b/skillopt_sleep/harvest.py @@ -143,11 +143,12 @@ def _detect_feedback(text: str) -> List[str]: # session, so the wrapper is unwrapped instead of dropped. _PASTED_CONTENT_RE = re.compile(r"]*>", re.IGNORECASE) -# Host-injected status notes and environment blocks. A message that is only -# a reminder is host noise, and a reminder prefixed to a real request must -# not ride into the mined intent as text the user never wrote. +# Host-injected status notes. A message that is only a reminder is host noise, +# and a reminder *prefixed* to a real request must not ride into the mined +# intent as text the user never wrote. The match is anchored by the caller: +# only the block that leads a message is removed. _SYSTEM_REMINDER_RE = re.compile( - r".*?\s*", re.IGNORECASE | re.DOTALL + r"]*>.*?", re.IGNORECASE | re.DOTALL ) # Tags the host injects as a whole user message: expanded slash-command @@ -170,14 +171,25 @@ def _detect_feedback(text: str) -> List[str]: def _strip_host_wrappers(text: str) -> str: - """Unwrap pasted user content and drop host-injected blocks. - - Only the two wrappers above are touched: a message keeps its text when it - is wrapped in a tag the host does not own. + """Unwrap pasted user content and drop the host's own leading notes. + + ```` tags only ever wrap text the user typed, so both the + opening and the closing tag are removed wherever they appear. A + ```` block is removed only while it *leads* the message, + which is how the host prepends it. The same text inside a prompt the user + wrote — an inline-code sample, a fenced XML example — is their content and + survives, so the harvested request stays what they typed. """ if "<" not in text: return text - return _PASTED_CONTENT_RE.sub("", _SYSTEM_REMINDER_RE.sub("", text)) + out = _PASTED_CONTENT_RE.sub("", text) + while True: + stripped = out.lstrip() + match = _SYSTEM_REMINDER_RE.match(stripped) + if match is None: + break + out = stripped[match.end():] + return out def _is_meta_prompt(text: str) -> bool: @@ -267,11 +279,12 @@ def _is_headless_replay(digest: "SessionDigest") -> bool: if digest.n_user_turns > 1: return False if digest.n_user_turns == 0: - # Every user turn was filtered as host noise or engine input. That is - # not a replay signal by itself: a session whose user turns were all - # host-wrapped still holds real assistant work (#318), so only a - # transcript with no work at all counts as engine-generated. - return digest.n_assistant_turns == 0 + # Every user turn was filtered as host noise or engine input, so there + # is no user intent to mine. Such a record must not be admitted: it + # would take a slot under --max-sessions and yield no task, displacing + # a real session (review of #318). A pasted brief is not this case — + # it now survives the filter as a user turn. + return True prompt = digest.user_prompts[0] if digest.user_prompts else "" for marker in _REPLAY_PROMPT_MARKERS: if marker in prompt: diff --git a/tests/test_sleep_engine.py b/tests/test_sleep_engine.py index 91662ba52..71476365b 100644 --- a/tests/test_sleep_engine.py +++ b/tests/test_sleep_engine.py @@ -191,18 +191,66 @@ def test_system_reminder_prefix_does_not_ride_into_the_intent(self): self.assertEqual(task.intent, request) self.assertNotIn("Status note", task.intent) - def test_session_without_user_turns_is_a_replay_only_when_it_has_no_work(self): - from skillopt_sleep.harvest import _is_headless_replay - - # issue #318: all user turns filtered out is not a replay signal on - # its own — real assistant work means the session is a real one. - worked = SessionDigest(session_id="s", project="/repo/example", - assistant_finals=["Done."], - n_user_turns=0, n_assistant_turns=2) - empty = SessionDigest(session_id="s", project="/repo/example") - - self.assertFalse(_is_headless_replay(worked)) - self.assertTrue(_is_headless_replay(empty)) + def test_host_only_transcript_does_not_take_a_session_slot(self): + from skillopt_sleep.harvest import harvest + + # Review of #318: a record with no user intent must not be admitted + # before the cap. It mines nothing, so admitting it turns a night into + # zero tasks by displacing a real session with a newer host-only one. + with tempfile.TemporaryDirectory() as tmp: + project_dir = os.path.join(tmp, "projects", "-repo-example") + os.makedirs(project_dir) + host_only = os.path.join(project_dir, "host-note.jsonl") + self._write_jsonl(host_only, [ + {"type": "user", "isMeta": True, + "timestamp": "2026-10-15T09:00:00Z", + "cwd": "/repo/example", "sessionId": "host-note", + "message": {"role": "user", + "content": "ok"}}, + {"type": "assistant", "timestamp": "2026-10-15T09:00:30Z", + "cwd": "/repo/example", "sessionId": "host-note", + "message": {"role": "assistant", "content": "Noted."}}, + ]) + real = os.path.join(project_dir, "real-task.jsonl") + self._write_claude_session(real, [ + ("user", "Fix the failing date parser test in module B."), + ("assistant", "Fixed."), + ]) + os.utime(host_only, (2_000_000, 2_000_000)) # newer + os.utime(real, (1_000_000, 1_000_000)) + + digests = harvest(os.path.join(tmp, "projects"), scope="all", limit=1) + + self.assertEqual([d.session_id for d in digests], ["real-task"]) + + def test_inline_code_system_reminder_example_is_user_text(self): + # Review of #318: the user quoting a reminder inside inline code was + # being deleted, which changed both the request and the mined intent. + request = ("How should I escape `note` " + "inside my transcript fixture?") + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "inline-example.jsonl") + self._write_claude_session(path, [ + ("user", request), + ("assistant", "Use html.escape()."), + ]) + digest = digest_transcript(path) + + self.assertEqual(digest.user_prompts, [request]) + + def test_fenced_system_reminder_example_is_user_text(self): + request = ("This is what our host writes before a real request:\n" + "```xml\n\ncontext\n\n```\n" + "Please make the parser strip it.") + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "fenced-example.jsonl") + self._write_claude_session(path, [ + ("user", request), + ("assistant", "Will do."), + ]) + digest = digest_transcript(path) + + self.assertEqual(digest.user_prompts, [request]) def _claude_session_with_injected_messages(self, tmp, follow_up): """A Claude Code session as written to disk when a skill is loaded.