mpfaffenberger / mpfaffenberger/code_puppy

pop_command and prune plugins duplicate lazy emit_* wrappers and tail tool-fragment pruning — and the two pruning implementations disagree

Open
#455 0 comments 0 reactions 0 assignees View on GitHub

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 ANY ToolReturnPart-only request or ANY ToolCallPart response, 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_calls at :72-125) — pops tail messages only when the fragment is genuinely orphaned (its tool_call_id has 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.