Skip to content

fix(hooks): AgentInvocation logs every background Agent spawn as subagent_stop (duration 0) — async-spawn detection misses the current tool_response, and start records are gone before AgentWatchdog can count them #2387

Closed

Description

@IvanLeontev-stack

Version

LifeOS 7.40.4 (main @ 5e2f2e8), Claude Code 2.1.30x (observed 2026-10-23; Agent tool schema unchanged in 2.1.310).

What is broken

AgentInvocation.hook.ts runs on PreToolUse and PostToolUse for Agent. Since v1.4.0 it knows PostToolUse fires at spawn time for a background agent and detects that case by matching tool_response against /Spawned successfully|running and will receive instructions via mailbox/i: on a match it logs subagent_spawned_async, otherwise subagent_stop with a computed duration.

In the current harness the match never succeeds for a background spawn. Every dispatch produces a subagent_start and a subagent_stop with duration_seconds: 0 in the same second — 7 of 7 dispatches across two sessions on 2026-10-23 — so subagent-events.jsonl records every background agent as a zero-second run, and anything built on those events (durations, the Pulse agents view) sees agents that start and stop at once.

The transcript's tool_result for the same spawn does contain both phrases ("Spawned successfully. (This tool result is internal metadata …)\nagent_id: …\nname: …\nThe agent is now running and will receive instructions via mailbox."), so the PostToolUse tool_response payload the hook receives must differ from the transcript text. I have not captured the raw payload — the hook does not log it.

A second thing I noticed while tracing it: the start record is deleted at PostToolUse in both branches (:204-211 runs before the async check, and the v1.4.0 header says the spawn-ack removes it by design, liveness coming from subagents/agent-*.jsonl mtimes). That means AgentWatchdog.ts, which counts active agents from the starts file, sees zero during a background run even after #2362 fixes the file name — the record is gone within the spawn second. Confirmed: agent-starts.json was {} while a 13-minute agent was working.

Where

  • LifeOS/install/hooks/AgentInvocation.hook.ts:217-220 (string coercion + regex), :204-211 (duration + delete starts[key]), :222-230 (the event).
  • LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts:41 (active agents = starts file; see fix(watchdog): read agent-starts.json #2362).

Repro

  1. Dispatch any background Agent (subagent_type: general-purpose, a brief that runs a few minutes).
  2. tail -2 MEMORY/OBSERVABILITY/subagent-events.jsonl → subagent_start and subagent_stop with "duration_seconds":0, same timestamp.
  3. cat MEMORY/OBSERVABILITY/agent-starts.json while the agent is still working → {}.

Suggested fix

Stop inferring the spawn from response text: with the current Agent tool every main-conversation dispatch is a background spawn, so PostToolUse:Agent is always the spawn-ack — log subagent_spawned_async there and take the real stop from the SubagentStop hook event (correlated by agent_id/name, since that payload lacks subagent_type/description), or from the transcript going quiet. If the response text stays a signal, log the raw tool_response shape once under a debug flag so the regex can be fixed against what the harness actually sends. For the watchdog, read liveness from the same subagents/agent-*.jsonl mtimes the header names instead of the starts file, so #2362 and this fix together give it something to watch.

🤖 Generated with Claude Code

Activity

  1. Shiv-61 commented on Oct 24, 2026

    @Shiv-61

    Hey @IvanLeontev-stack , Will this work

    1. Stop regex-matching tool_response. With the current Agent tool, every main-conversation dispatch is a background spawn, so PostToolUse:Agent is always the spawn-ack. Always log subagent_spawned_async there. Don't compute a duration and don't delete starts[key] (:204-211).

    2. Take the real stop from the SubagentStop event. Log subagent_stop and compute the duration there, correlating by agent_id/name (that payload lacks subagent_type/description). The transcript going quiet also works as a fallback signal.

    3. Fix the watchdog's liveness source. Make AgentWatchdog.ts:41 read subagents/agent-*.jsonl mtimes instead of agent-starts.json, which is {} within the spawn second. Combined with fix(watchdog): read agent-starts.json #2362, the watchdog then has something real to watch.

    4. If response text stays a signal, log the raw tool_response shape once behind a debug flag, so the regex can be fixed against what the harness actually sends.

  2. danielmiessler commented on Oct 28, 2026

    @danielmiessler
    Owner

    Note

    🤖 Kai (AI assistant) responding on behalf of Daniel Miessler

    Thanks @IvanLeontev-stack, and @Shiv-61 for the proposal. Both halves have changed since 7.40.4. First, the spawn regex now matches the harness's current Async agent launched wording; that fix arrived in v7.51.0. Second, when the LifeOSHooks plugin is active, this command hook stands down. The plugin reads start and stop from Claude Code's own agent list: it logs subagent_start when an agent spawns, checks the list every two seconds, and logs one subagent_stop per run with the real duration and status. That's the approach @Shiv-61 suggested.

    The watchdog reading the wrong file name is PR #2362, where we chose to retire the watchdog instead of fixing it. Deleting a background agent's start record when the spawn is acknowledged is by design, as the hook header explains. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions