mpfaffenberger / mpfaffenberger/code_puppy
pop_command and prune plugins duplicate lazy emit_* wrappers and tail tool-fragment pruning — and the two pruning implementations disagree
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Theme
plugins/pop_command/register_callbacks.py and plugins/prune/register_callbacks.py both edit message history and share two copy-pasted layers (jscpd: pop:10-31 <-> prune:27-48).
1. Lazy messaging wrappers (verbatim clone)
Both files define identical 4-function shims (pop_command/register_callbacks.py:10-31, prune/register_callbacks.py:27-48):
def emit_error(message: Any) -> None:
from code_puppy.messaging import emit_error as _emit_error
_emit_error(message)
# ... emit_info / emit_success / emit_warning, same pattern
If lazy import is genuinely needed (import-order at plugin load), that is a one-module concern: add code_puppy/plugins/_messaging.py with the four wrappers (or make code_puppy.messaging itself lazy via module __getattr__) and have both plugins import from there. Right now a third history plugin will copy these 22 lines again. Note no other plugin needed this workaround — every other plugin imports from code_puppy.messaging import emit_info directly, so it is worth checking whether the wrappers are needed at all.
2. _prune_dangling_tool_fragments — same name, same purpose, different semantics
pop_command/register_callbacks.py:71-88— pops tail messages whenever they contain ANYToolReturnPart-only request or ANYToolCallPartresponse, regardless of whether the counterpart still exists in history.prune/register_callbacks.py:128-143(with helpers_collect_tool_ids/_has_orphaned_returns/_has_orphaned_callsat :72-125) — pops tail messages only when the fragment is genuinely orphaned (itstool_call_idhas no live partner).
Both exist to satisfy the same provider invariant (every ToolCallPart needs a matching ToolReturnPart — the comment in prune even names Anthropic). The pop version over-prunes: /pop 1 on a tail of [..., response+tool_call, request+tool_return] first slices the tool_return, then its fragment pass removes the now-dangling tool_call — fine — but on a complete call/return pair left mid-history its blunt "any tool parts at tail" test can strip messages the prune version would correctly keep. Two commands maintained by the same team give different history-repair results for the same input.
Proposed shared abstraction
Create code_puppy/agents/history_pruning.py (history surgery is core-domain logic, not plugin-specific):
def prune_dangling_tool_fragments(history: list[Any]) -> tuple[list[Any], int]:
"""Strip genuinely orphaned tool-call/return fragments from the tail.
Canonical implementation: the id-matching version currently in
plugins/prune/register_callbacks.py (correct superset of pop's heuristic).
"""
Move _collect_tool_ids / _has_orphaned_returns / _has_orphaned_calls there, have both plugins (and any future compaction code) import it, and delete pop's blunt variant. This also gives the invariant a single unit-test home.
Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing plugins/pop_command/register_callbacks.py with plugins/prune/register_callbacks.py, especially the duplicated messaging wrappers and the pruning helpers. Trace how both callbacks edit history and verify the id-matching behavior against the provider invariant described in the issue. Done means one shared history-pruning implementation and one messaging-wrapper decision are used consistently by both plugins, with focused unit coverage for the invariant.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100