MoonshotAI / MoonshotAI/kimi-cli

fix(hooks): PostToolUse / PostToolUseFailure tasks collected by GC before completion

Open Beginner friendly
#2,564 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
11.4k
Forks
1.3k
Avg merge
9h 47m
Merged PRs (30d)
2

Description

Describe the bug

PostToolUse and PostToolUseFailure hooks registered in config.toml are silently dropped — the subprocess either never starts or is killed mid-execution. The hooks fire non-deterministically: sometimes they run, sometimes they don't.

Root cause

In kimi_cli/soul/toolset.py (lines 576–591 in v1.49.0), both hooks use bare asyncio.create_task() with the result stored in a local variable _hook_task:

# --- PostToolUse (fire-and-forget) ---
_hook_task = asyncio.create_task(
    self._hook_engine.trigger(
        "PostToolUse",
        matcher_value=tool_name,
        input_data=events.post_tool_use(...),
    )
)
_hook_task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)

return ToolResult(tool_call_id=tool_call.id, return_value=ret)  # ← _hook_task goes out of scope

The return on the next line causes _hook_task to go out of scope. Since asyncio holds tasks in a WeakSet, Python's GC can collect the still-pending task before the hook subprocess completes.

The fix already exists in the codebase

HookEngine.fire_and_forget_trigger() (in kimi_cli/hooks/engine.py:93) was created specifically for this pattern — its docstring literally describes this exact bug:

def fire_and_forget_trigger(self, ...):
    """Trigger a hook in the background and keep a strong reference to the
    task. asyncio holds tasks in a WeakSet, so naively writing
    ``asyncio.create_task(engine.trigger(...))`` and discarding the local
    variable lets Python's GC collect the still-pending task ...
    Use this helper any time the caller wants to fire a hook without
    awaiting its completion.
    """

SubagentStop in kimi_cli/subagents/runner.py:301 already uses fire_and_forget_trigger() correctly. PostToolUse and PostToolUseFailure were not migrated.

Suggested fix

Replace asyncio.create_task() + add_done_callback() with self._hook_engine.fire_and_forget_trigger() in two places in toolset.py:

 # --- PostToolUseFailure (fire-and-forget) ---
-_hook_task = asyncio.create_task(
-    self._hook_engine.trigger(
-        "PostToolUseFailure",
-        ...
-    )
-)
-_hook_task.add_done_callback(
-    lambda t: t.exception() if not t.cancelled() else None
-)
+self._hook_engine.fire_and_forget_trigger(
+    "PostToolUseFailure",
+    ...
+)
 # --- PostToolUse (fire-and-forget) ---
-_hook_task = asyncio.create_task(
-    self._hook_engine.trigger(
-        "PostToolUse",
-        ...
-    )
-)
-_hook_task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)
+self._hook_engine.fire_and_forget_trigger(
+    "PostToolUse",
+    ...
+)

To reproduce

  1. Add a PostToolUse hook to config.toml:
[[hooks]]
event = "PostToolUse"
command = "python3 /path/to/my-hook.py"
matcher = "^kanban_move$"
timeout = 30
  1. Run a session that triggers kanban_move multiple times in rapid succession (batch operations).

  2. Observe that some hook invocations never produce output / never start the subprocess.

Affected versions

  • v1.48.0
  • v1.49.0 (latest)

Environment

  • macOS 15 (Apple Silicon)
  • Python 3.14
  • kimi-cli 1.48.0 / 1.49.0

Contributor guide

Open the contributing guide

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 in kimi_cli/soul/toolset.py at the PostToolUse and PostToolUseFailure hook calls, then compare them with HookEngine.fire_and_forget_trigger() in kimi_cli/hooks/engine.py and its existing use in kimi_cli/subagents/runner.py. The work is complete when both affected hook paths use the established task-lifetime handling and rapid invocations no longer disappear before completion.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.