Repository navigation
fix(sleep): stop a re-run hook template from filling the session cap - #323
Ansu (SU-AN-coder) wants to merge 2 commits into
Conversation
|
Reviewed Please keep a safe default and make uncertain recurrence filtering explicitly opt-in or restrict it to evidence that distinguishes the intended hook. Preserve real recurring templates and use counters that do not overstate producer identity. With recurrence disabled, limit=1 still digests the whole eligible window (40 files in the parent control versus one on main); restore the bounded cap behavior in that mode. This is a request for narrower scope, not a maintainer rewrite. #319 remains open. |
…icrosoft#319) A local hook that summarises the previous session on a timer writes one single-turn transcript per run. Its only user prompt is the hook's own template: identical leading text, a fresh log appended. Neither the engine replay markers nor the sub-agent markers match it, and in the desktop app those runs carry `entrypoint: "claude-desktop"` like interactive sessions, so it was harvested as a user task. Because harvest walks newest first and stops at `--max-sessions`, an hourly hook filled the cap and pushed real sessions out of the night entirely. `harvest()` now drops single-turn sessions whose first 120 characters of prompt recur across at least three sessions of the same harvest window, before the cap is applied. The whole window is scanned first: a template can only be recognized by comparing it with the rest of the window, and the repeats must not be counted against the cap they used to fill. A session with a follow-up user turn is never matched, and neither is a prompt shorter than the prefix — a real one-shot request repeats entirely instead of sharing a long boilerplate header. `SKILLOPT_SLEEP_REPEAT_MIN_SESSIONS` tunes the threshold (default 3) and `0` disables the filter. `harvest()` takes an optional `stats` out-parameter that `harvest` and the `run` / `dry-run` progress line report, so a thin night says why: `harvest done: sessions=120 (skipped: 297 machine-generated, 2 past the session cap)`. The harvest filters that already existed were undocumented — no .md file named `SKILLOPT_SLEEP_AGENT_MARKERS`, `SKILLOPT_SLEEP_NEG_FEEDBACK` or `SKILLOPT_SLEEP_POS_FEEDBACK`. They are now documented in the sleep guide and the Claude Code plugin README, with the hourly-summary-hook example.
Two corrections from review, both about the default path rather than the mechanism: * Recurrence is not evidence of who wrote a prompt. Three human code-review requests that share one long checklist prefix and differ in the task payload are structurally identical to a hook re-running itself: one single-turn session each, >120 shared leading characters, a different tail. Dropping those loses real user tasks, which is worse than harvesting a hook's summary sessions, so the filter is now off unless a project opts in with SKILLOPT_SLEEP_REPEAT_MIN_SESSIONS (3 documented as the usual value; 0 disables an exported value again). The docstring and both READMEs say why, and name the counter-example. * With the filter off, harvest keeps its single bounded pass: it digests newest-first and stops at `limit`, so `--max-sessions 1` reads one transcript instead of the whole eligible window. The full-window scan now happens only when the filter is opted into, which is the mode that needs it. Counters no longer claim a producer: `repeated_template_sessions` / "repeated-template sessions" replaces `machine_generated_repeats` / "machine-generated", in the stats dict and in the progress note. Measured on the pre-change code with the new tests: 9 failed, 6 passed — including the shared-checklist case (3 requests dropped) and the bounded-cap case (40 transcripts read for `limit=1`). After: 15 passed. Full suite: 1,846 passed, 11 skipped, 368 subtests.
d3eeb3f to
7aabc1d
Compare
|
Rebased onto Default is safe: the filter is off unless a project opts in. You were right that recurrence is not attribution. Three human requests for a code review against one long checklist share a single-turn shape, a >120-character common prefix and a different task after it — the same evidence a hook's re-runs leave. So The bounded cap is back in the disabled mode. With the filter off,
Counters no longer name a producer. Rebase. The only conflict was one import line in Verification on
One thing I did not do: I considered one more signal that would let the filter stay on by default — the hook's runs use no tools, while a human review request in Claude Code normally reads files — but I cannot verify that against the three records you reproduced, so it would be a second guess stacked on the first. I left the default off instead. If you would rather have that condition as a default-on narrowing, say so and I will add it with the same before/after measurement. The consequence, stated in the PR body: with the safe default, a folder like the one in #319 still harvests the hook's runs and |
Summary
harvest()had no filter for single-turn transcripts written by local tooling. #319 measured what that costs: in one real project folder, 274 of 324 transcripts were an hourly hook's session summaries, arriving one per hour. Their only user prompt is the hook's own template — identical leading text, the run's log appended.Neither existing filter matches them.
_is_headless_replay()catches engine replay markers, or a sub-3 s session with a prompt under 200 characters; these prompts are ~400 characters._is_agent_session()matches the claude-mem markers and the plugin's own command body. And in the Claude Code desktop app these runs carryentrypoint: "claude-desktop", like interactive ones, so theentrypointfilter from #62 cannot separate them either (I did not add one, for that reason).Because
harvest()walks newest first and stops at--max-sessions, they filled the cap. Two real sessions plus three summary sessions withlimit=3returned only the three summary sessions, andheuristic_mine()turned the template into "recurring tasks" — the issue's own output, reproduced onae2f766below.What changed
1.
harvest()can drop a repeated single-turn template before the cap (skillopt_sleep/harvest.py) — opt-in, off by default. A single-turn session that shares its first 120 characters of prompt with at leastNsessions of the same harvest window is treated as one template re-running, and dropped beforelimitis applied so it cannot consume the cap it used to fill.Recurrence is not evidence of who wrote a prompt, though, and the review named the counter-example: three human requests for a code review share one long checklist prefix and differ only in the task payload. That is structurally identical to a hook re-running itself — one turn each, >120 shared leading characters, a different tail — so labelling those records machine-generated is a guess, and losing a real task is worse than harvesting a hook's summary sessions. Hence:
_DEFAULT_REPEAT_MIN_SESSIONS = 0— the filter does nothing unless a project opts in withSKILLOPT_SLEEP_REPEAT_MIN_SESSIONS=N(3 is documented as the usual value,0disables an exported value again);2.
harvest()reports what it filtered, through an optionalstatsout-parameter filled in place:candidates,repeated_template_sessions,skipped_by_cap,sessions. The counter is named for what was measured — a repeated template — not for a producer the harvester cannot identify; the earliermachine_generated_repeats/ "machine-generated" wording is gone from the stats dict, the progress note and the docs.format_harvest_note()renders it:That line appears in
skillopt-sleep harvestand in therun/dry-runprogress line (harvest done: sessions=120 (skipped: …)).3.
harvest_for_config()forwardsstatsto the Claude source. The other sources (codex, copilot, cursor, pi, opencode) have no repeat filter of their own yet and leavestatsuntouched, which is stated in its docstring; the callers only print when it is non-empty.4. The filters that already existed were undocumented. I checked before writing:
grep -rl SKILLOPT_SLEEP_AGENT_MARKERS --include=*.md .returned nothing, and the same forSKILLOPT_SLEEP_NEG_FEEDBACKandSKILLOPT_SLEEP_POS_FEEDBACK— all three were named only inharvest.pycomments. They are now in the harvest documentation (docs/sleep/README.md, Harvest filters (environment)) and the plugin README (plugins/claude-code/README.md, Sessions you did not type), with the hourly-summary-hook example from this report.The cap stays bounded when the filter is off
With the default (off),
harvest()keeps the single pass older releases ran: it digests newest-first and stops atlimit, so--max-sessions 1reads one transcript, not the whole eligible window. The full-window scan happens only in the opted-in mode, which needs it: a template can only be recognised by comparing it against the rest of the window, and the repeats must not be scored against the cap. Both behaviours are pinned by tests (test_default_cap_still_bounds_how_many_transcripts_are_digested: 40 transcripts on disk,limit=1, exactly 1 digest call;test_opt_in_scans_the_window_so_the_cap_can_be_refilled: 40 calls, the deliberate trade).Verification
New tests:
tests/test_harvest_machine_repeats.py(15).On the pre-review commit
17745c1(the rebased filter, default on), with the new tests present: 9 failed, 6 passed.The six that pass on both commits are named here rather than left to look like evidence:
test_summary_sessions_do_not_fill_the_session_cap,test_the_mined_intents_are_the_real_ones,test_short_repeated_one_shot_prompts_are_kept,test_multi_turn_session_sharing_the_prefix_is_kept,test_threshold_follows_the_env_varandtest_opt_in_scans_the_window_so_the_cap_can_be_refilled. They exercise the mechanism with the variable exported, and the old code already filtered with the variable set — they pin the filter, not the default change.test_short_repeated_one_shot_prompts_are_keptis a false-positive guard and is green by construction on any commit.The issue's reproduction script, base vs. branch (two real sessions + three hourly summary runs, no model calls, no network):
The last block is the honest cost of the safe default: #319's behaviour is unchanged until a project opts in — the hourly runs still fill
limit=3. On the pre-review code the summary sessions also reachedheuristic_mine()and produced "recurring tasks" from the hook's template; with the filter off they still do. That is why this PR references #319 rather than closing it: it ships the opt-in mechanism, the diagnostics and the documentation, and leaves the default attribution to the people who can recognize their own hook.Gate set on
7aabc1d:Scope
Refs #319 — opt-in mitigation, not a default attribution. The other half of that report — the
entrypointproposal from #62 — I deliberately did not implement: those runs carry"claude-desktop", so it would not have caught anything here. #312 (per-source harvest checkpoint) and #318 (pasted briefs) are separate changes; this PR does not touch them.I read every line of the diff myself. The branch is
SU-AN-coder:fix/harvest-machine-generated-repeats-319at7aabc1d, rebased ontomain(1a44050) and the owner has it.Files
skillopt_sleep/harvest.pystatsout-parameter,format_harvest_note()skillopt_sleep/harvest_sources.pystatsto the Claude sourceskillopt_sleep/cycle.pyharvest done:lineskillopt_sleep/__main__.pyskillopt-sleep harvesttests/test_harvest_machine_repeats.pydocs/sleep/README.md,plugins/claude-code/README.md