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/3] 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/3] 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. From 21bd085f790faf08c31d26cc45752a8d1f8bbd4d Mon Sep 17 00:00:00 2001 From: Ansu <230804140223@stu.nepu.edu.cn> Date: Sat, 24 Oct 2026 11:14:37 +0800 Subject: [PATCH 3/3] fix(sleep): unwrap only a leading pasted block, keep literal tags in a request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapper is the host's while it leads the message, not wherever the tag pair appears. A request that asks for a parser test on `LITERAL_XML_VALUE` — inline code, a fenced XML example, a fixture carrying attributes — lost its tags in digest_transcript(), harvest() and the mined intent. Unwrapping now follows the leading-only rule the system-reminder block already uses. Review on f5f1f9a: four literal cases failed there and pass here; pasted-only, pasted-then-follow-up and the Copilot privacy cases still pass. --- skillopt_sleep/harvest.py | 43 ++++++++++++++++++++++++-------------- tests/test_sleep_engine.py | 29 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/skillopt_sleep/harvest.py b/skillopt_sleep/harvest.py index da0ac9dc4..52ed0d7e4 100644 --- a/skillopt_sleep/harvest.py +++ b/skillopt_sleep/harvest.py @@ -140,8 +140,13 @@ def _detect_feedback(text: str) -> List[str]: # 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) +# session, so the wrapper is unwrapped instead of dropped — but only while it +# leads the message, because the same tag pair inside a prompt the user wrote +# is their content (#321 review: four literal-example cases). +_PASTED_CONTENT_LEAD_RE = re.compile( + r"]*>(.*?)]*>", + re.IGNORECASE | re.DOTALL, +) # 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 @@ -171,25 +176,31 @@ def _detect_feedback(text: str) -> List[str]: def _strip_host_wrappers(text: str) -> str: - """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. + """Unwrap a host's leading pasted block and drop its own leading notes. + + Both are positioned, not textual. A ```` wrapper is + unwrapped only while it *leads* the message, which is how the host + prepends it — and a ```` block only while it leads as + well. Neither rule reaches inside the text the user wrote: a request that + names the tag pair literally (an inline-code sample, a fenced XML + example, a fixture with attributes) keeps its tags, so the harvested + request and the mined intent stay what they typed. Stripping every + occurrence changed both. """ if "<" not in text: return text - out = _PASTED_CONTENT_RE.sub("", text) + out = text while True: stripped = out.lstrip() - match = _SYSTEM_REMINDER_RE.match(stripped) - if match is None: - break - out = stripped[match.end():] - return out + pasted = _PASTED_CONTENT_LEAD_RE.match(stripped) + if pasted is not None: + out = pasted.group(1) + stripped[pasted.end():] + continue + reminder = _SYSTEM_REMINDER_RE.match(stripped) + if reminder is not None: + out = stripped[reminder.end():] + continue + return out def _is_meta_prompt(text: str) -> bool: diff --git a/tests/test_sleep_engine.py b/tests/test_sleep_engine.py index 71476365b..bece0e973 100644 --- a/tests/test_sleep_engine.py +++ b/tests/test_sleep_engine.py @@ -114,6 +114,35 @@ def test_meta_prompt_keeps_pasted_user_content(self): "\nInstall tool X into a dedicated folder, pin the version.\n", ) + def test_literal_pasted_content_in_a_request_is_user_text(self): + # #321 review: the wrapper is the host's only while it leads the + # message. A request that names the tag pair literally — inline code, + # a fenced XML example, a fixture carrying attributes — keeps it, in + # the harvested request and in the mined intent alike. + from skillopt_sleep.harvest import _strip_host_wrappers + + inline = ("Add a parser test for the literal inline example " + "`LITERAL_XML_VALUE`.") + fenced = ("Add a parser test for this fenced example:\n\n```xml\n" + "LITERAL_XML_VALUE\n```") + attributed = ('Keep `LITERAL_XML_VALUE' + '` in the fixture.') + for text in (inline, fenced, attributed): + self.assertEqual(_strip_host_wrappers(text), text) + + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "literal-session.jsonl") + self._write_claude_session(path, [ + ("user", inline), + ("assistant", "Plan: ..."), + ("user", "Yes, go ahead."), + ("assistant", "Done."), + ]) + digest = digest_transcript(path) + + [task] = heuristic_mine([digest]) + self.assertIn("LITERAL_XML_VALUE", task.intent) + def test_meta_prompt_filters_only_known_host_tags(self): self.assertTrue(_is_meta_prompt("ok")) self.assertTrue(_is_meta_prompt(