Skip to content

Hooks: PreCompact/PostCompact/SessionStart(compact) fire for a subagent's compaction without agent fields; SubagentStop fires for internal summarizer calls with a never-created agent_transcript_path #91910

Open

Description

@mrinaldhillon

Summary

Three related hook-payload gaps around subagent compaction and the harness's internal summarizer calls. Observed on Claude Code 2.1.259, Linux, with project hooks registered for PreCompact, PostCompact, SessionStart (matcher compact) and SubagentStop.

1. PreCompact and SessionStart(compact) fire for a background subagent's auto-compaction with the main loop's payload shape

When a background subagent (Agent tool) auto-compacts, the PreCompact hook and the SessionStart hook with matcher: compact both run. The payload carries the parent session's session_id, transcript_path and cwd, and nothing that identifies the subagent: no agent_id, no agent_type, no agent_transcript_path, no flag that says the compaction is a subagent's. A hook cannot tell a subagent's compaction from the main loop's without parsing the transcript tail itself.

Consequence: a PreCompact hook that checkpoints state, or holds a compaction with {"decision":"block"}, applies to every subagent's compaction as well. A SessionStart(compact) hook that re-injects resume state injects the main loop's state into the subagent. The workaround is reading the transcript tail for isSidechain records, which is a heuristic.

Ask: carry agent_id, agent_type and agent_transcript_path on PreCompact, PostCompact and SessionStart(compact) when the compaction belongs to a subagent, matching what SubagentStop already carries. A boolean is_subagent would also do.

2. PostCompact fires for the subagent compaction with the same gap

PostCompact fires roughly 35 ms after SessionStart(compact) for a subagent's compaction, with the parent's fields plus compact_summary, and no agent identity. Same ask as above.

3. SubagentStop fires for internal model calls that are not subagents

The activity summarizer (the short progress strings in the task display) and the compaction summarizer (the <analysis> output) each fire SubagentStop with an empty agent_type, a fresh agent_id, and an agent_transcript_path that is never created on disk. In one long session, 342 of the 381 distinct agent_transcript_path values a SubagentStop hook recorded were such phantoms. Only 39 were real dispatches with a transcript file.

Consequence: a SubagentStop count is not a dispatch count, and any hook that opens agent_transcript_path reports hundreds of missing files. The workaround is discriminating on agent_type == "" and on the presence of the agent-<id>.meta.json sibling, which only real subagent transcripts have.

Ask: either do not fire SubagentStop for internal summarizer calls, or mark them (for example internal: true, or agent_type: "internal:activity" / "internal:compaction"), and do not populate agent_transcript_path with a path that will never exist.

4. Minor

  • SubagentStop fires before the subagent transcript's final record is appended, so a hook that reads the file at stop time sees an incomplete transcript. A flag on the payload once the file is final, or a post-stop event, would let hooks stop polling.
  • A project-scope settings.json with env.TMPDIR logs "project-scoped settings can't set this key" on every start since 2.1.257. The docs still list env as project-settable without naming the excluded keys.

Environment

  • Claude Code 2.1.259
  • Linux
  • Hooks: PreCompact, PostCompact, SessionStart (compact), SubagentStop, registered in project settings.json

Activity

  1. 0xdhx commented on Sep 12, 2026

    @0xdhx

    Adding the emit-site mechanism for §3, read from the 2.1.269 bundle, plus confirmation it is still live there. Symbols are minified names from the installed native binary, so they are for orientation, not a permalink.

    Why every internal fork produces a SubagentStop: the stop-hook dispatcher decides Stop vs SubagentStop on whether the context carries an agent id at all —

    async function*NZ(e,n,r=Cp,s=!1,d,m,_,E,A="turn_end"){let P=d?"SubagentStop":"Stop"; …
      me=d?{...re,hook_event_name:"SubagentStop",stop_hook_active:s,agent_id:d,agent_transcript_path:yp(d),agent_type:E??"",…}

    — and the shared fork helper that runs every internal one-turn query mints a fresh id for the forked context while copying no type:

    // HTn(parentCtx, overrides) → forked toolUseContext
    agentId:n?.agentId??ly(),  agentType:n?.agentType,   // FE never passes agentType → undefined → ""

    FE is the helper behind the activity summarizer (forkLabel:"agent_summary", maxTurns:1, skipTranscript:!0), the away summary, and compact / reactive-compact. With skipTranscript the transcript writer is never opened (mt=P?void 0:ly(d)), so the agent_transcript_path derived from that fresh id is a file nothing will ever write. The activity summarizer is a setTimeout loop at Jks=30000 per running background agent ([AgentSummary] Timer fired for agent …), which is where the ~30 s cadence comes from.

    Measured on 2.1.269 (matcher-less SubagentStop hook logging agent_id / agent_type / agent_transcript_path + test -e): 165 of 176 stops across four SDK-hosted sessions had agent_type: "" and a never-created path; in one session 50 of its 53 phantoms fired inside a running background agent's lifetime at ~31 s intervals (17:54:00, 17:54:31, 17:55:03, 17:55:34, …), the other 3 coincided with away-summary events.

    Control — nested Agent-tool spawns are unaffected, in case anyone reads the phantoms as depth-2 spawns: a background agent that spawns one child produced subagents/agent-<child>.jsonl plus agent-<child>.meta.json ("parentAgentId":"…","spawnDepth":2), and both stops arrived with a real agent_type and an existing transcript. So the .meta.json discriminator in the original report is the right one — real spawns have it, forks never do.

    Given the branch, the smallest fixes look like: pick SubagentStop in NZ only when agentType is set (or an explicit real-subagent flag from the Agent-tool spawn path), and/or forward forkLabel as agent_type (internal:agent_summary, internal:compact), and omit agent_transcript_path whenever skipTranscript is on. Related from the outside: #87065 (matcher bypass on the empty string) and #89555 (fresh-id orphan stops) are the same emit shape.

  2. pierzchalski commented on Sep 16, 2026

    @pierzchalski

    Looks like InstructionsLoaded and Notification hooks also don't get subagent identity information. #16424 (closed as complete) was meant to cover this sort of thing, should it be reopened? This comment sounds like every hook event from a subagent was intended to have at least agent_id and agent_type.

    I'm currently working around a lack of this information by hijacking PreToolUse hooks and praying they happen close enough to the event I actually want to respond to.

  3. raichominev commented on Sep 22, 2026

    @raichominev

    Still present on 2.1.275 (Windows 11, Claude desktop app). Three additions that may help.

    Where the fields get lost. In 2.1.275 the shared hook-input builder takes agent_id from the tool-use context that the caller passes, and agent_type from that context or else from the main thread's --agent type. PostToolBatch passes the subagent's context, so a subagent's tool hooks do carry agent_id and agent_type. PreCompact and the SessionStart(compact) that ends a compaction build their input without that context. The hook runner receives the subagent's id, but the payload does not contain it. In these two events agent_type, when present, is the parent's --agent type, so it cannot identify a subagent either.

    What it cost. One session had 5 subagent auto-compactions between 2026-09-16 and 2026-09-22, under a plugin that keys its state by session_id:

    • SessionStart(compact) pasted the parent's resume prompt, next steps included, into each of the 5 subagents, and reset the parent's per-cycle state.
    • PreCompact recorded the parent's context size as the learned compaction point. The learned point fell to 287K and then 396K while the real point was about 967K, so later sessions asked for their handoff far too early. The same hook can also hold a subagent's compaction with {"decision":"block"}, because its hold condition belongs to the parent.

    Workaround for other hook authors. On PreCompact with trigger: "auto", treat the compaction as a subagent's when a transcript under <session>/subagents/**/agent-*.jsonl that changed in the last 60 s is fuller (last assistant usage divided by the context window) than the main transcript. Pass the result to the following SessionStart(compact) in a small record file. A replay of all 59 compactions on this machine (54 main, 5 subagent) classified each one correctly. It is still a heuristic, and an agent_id on these events would remove the need for it. Implementation: raichominev/compaction-handoff@7d62655

    Related: each of these subagent compactions also triggered the transcript replay of #92089 in the parent's transcript (details there).

  4. kargh commented on Sep 22, 2026

    @kargh

    Still present on 2.1.280 (Windows 11, VS Code extension, native binary). Adding the exact call site, a second gap that blocks the obvious workaround, and the signals I ruled out, all measured in throwaway claude -p sessions with --setting-sources project and a probe hook on every event.

    The fix is one argument. The shared hook-input builder is function Ul(e,n,r,s) and returns agent_id: s?.agentId, agent_type: s?.agentType ?? <main-thread type>. PreToolUse calls it as Ul(s.session, ne(), g, s), passing the tool-use context. PreCompact calls it as Ul(e, ne()) inside the function that runs the PreCompact hooks, while the context r is in scope: the same function passes agentId: r.agentId to the hook runner on the next line. Passing r as the fourth argument would populate agent_id/agent_type for a subagent's compaction. PostCompact builds its input the same way. (Minified names are from the 2.1.280 bundle, for orientation only.)

    Second gap: session-scoped hooks are ignored by PreCompact. The natural workaround is a hook registered by a skill's frontmatter hooks:, since those are stored per context (the resolver keys them by ctx.agentId ?? session_id). I verified the scoping works for tool events: a PreToolUse hook registered by a skill the main thread invoked fired for the main thread's Bash calls and never for a foreground subagent's. The same skill's PreCompact hook never fired at all, for the main thread or the subagent. The PreCompact runner call is not passed the session hook registry, unlike e.g. SessionEnd, so skill-scoped PreCompact hooks are silently dropped.

    A block is inescapable for a subagent. Every compaction path honors a PreCompact block, including the reactive path that runs on prompt-too-long. So a {"decision":"block"} whose condition belongs to the parent (for example "don't compact while a subagent is running") also blocks that subagent's own compaction, with no fallback until the subagent hits its hard limit.

    Signals that do not identify the caller (measured during a foreground subagent's compaction attempts, main thread idle):

    • All 91 environment variables are identical for main and subagent PreCompact calls, including CLAUDE_PID and CLAUDE_CODE_SESSION_ID. The stdin payload was 867 bytes on every call.
    • session_id, transcript_path, prompt_id and scratchpad_dir are the parent's. A subagent's own system prompt and $CLAUDE_CODE_SESSION_ID also report the parent's scratchpad directory and session id.
    • cwd is per-context and differs only for an isolation: "worktree" subagent (it reports .claude/worktrees/agent-<id>). A plain subagent's cd does not change it.
    • A blocked compaction writes nothing to the calling context's transcript.

    Ask: pass the context into the input builder for PreCompact, PostCompact and SessionStart(compact), and pass the session hook registry to the PreCompact runner so skill-scoped hooks fire in the compacting context.

  5. rezzminator commented on Sep 24, 2026

    @rezzminator

    Still present on 2.1.281 and 2.1.282 (macOS, native binary). Same call site as reported above: the PreCompact input is built as rc(e, ne()), the shared builder rc(e, n, r, s) without its fourth argument, so agent_id and agent_type are dropped even though the hook runner receives agentId: r.agentId in the same function.

    Measured impact, from a PreCompact hook that holds each compaction until the compacting party reaches its own token threshold (one threshold for the main loop, another for subagents), with the party inferred from which transcript under <session>/subagents/ was written most recently:

    • One subagent at a time: every decision was right (the estimate matched the transcript's recorded usage exactly).
    • An orchestrator running ~10 subagents in parallel: 4 of 19 subagent compactions were allowed early (at 120K–136K against a 150K threshold) because another subagent past its threshold was named for the attempt. The hook's log also shows subagents a second old (~6K tokens) named for other agents' attempts.
    • No transcript-timing heuristic can close this: the compacting agent's own attachment line is stamped at the moment of the attempt but is not yet flushed when the hook reads, so the on-disk state of the compacting agent looks like any other working agent's.

    With agent_id on PreCompact (as PreToolUse, PostToolBatch and SubagentStart already carry it), a per-agent compaction policy becomes a transcript read instead of a guess. Passing the tool-use context as the fourth argument would do it.

  6. cbrscn commented on Sep 25, 2026

    @cbrscn

    Still reproduces on Claude Code 2.1.281 (Windows, VS Code extension native binary; same code in 2.1.282).

    Root cause as far as the bundled JS shows: the shared hook-input builder takes agent fields from its 4th argument (agent_id from s?.agentId, agent_type from s?.agentType ?? mainThreadAgentType), but the PreCompact input and the SessionStart(compact) input are built without that argument, so agent_id is never set for them, although the hook input schema says agent_id is "Present only when the hook fires from within a subagent". session_id/transcript_path are the parent's, agent_transcript_path exists only on SubagentStop, and no agent env var reaches the hook command.

    Impact measured in one long session: 15 compactions in ~24 h, 14 of them in background subagents (compact_boundary in subagents/agent-*.jsonl), 1 in the main loop. Our PreCompact hook checkpointed main-loop state on every one of them, and our SessionStart(compact) hook injected the main loop's resume state into the subagents. An agent_id filter in both hooks never fired once.

    Written and posted by Claude (Claude Code, Opus 5.5) on behalf of @cbrscn.

  7. Nitewriter commented on Sep 29, 2026

    @Nitewriter

    Observed on 2.1.281 (not re-tested on 2.1.284 or 2.1.285; the 2.1.285 notes list no change here): the gap in this thread's
    item 1 is worse than "no agent_id" in one specific shape. When an in-process subagent (one
    launched by --agent <name> inside the same process, not a separate claude invocation) runs
    its own auto-compaction, the SessionStart hook fired for that compaction carries no agent_id
    and an agent_type equal to the main thread's --agent value — not empty, not the
    subagent's own type, and not omitted. So a hook that gates on agent_type (a reasonable-looking
    workaround for the missing agent_id) doesn't just fail to identify the subagent — it positively
    misidentifies the subagent's compaction as the main thread's.

    Impact we hit: a SessionStart hook meant to re-inject an instruction only for a freshly
    launched main session (never for a subagent restarting after compaction) read agent_type as the
    main session's own --agent value and fired anyway, injecting a main-session-only instruction
    into the subagent immediately after its compaction.

    Minimal repro shape:

    1. Start claude --agent X.
    2. Register a SessionStart hook (matcher compact) that logs agent_id and agent_type.
    3. Let an in-process subagent under that session compact.
    4. Observed: agent_type: "X", no agent_id — field-identical to the main thread's own
      SessionStart(compact), so nothing in the payload distinguishes the two.

    This is the same event family already tracked here (SessionStart matcher compact), just a
    sharper statement of the consequence: an agent_type-keyed workaround for the missing agent_id
    is not just blind to the subagent case, it is wrong in a way that actively fires main-thread-only
    logic inside the subagent.

  8. putao520 commented on Oct 5, 2026

    @putao520

    Extending the event-family coverage of this report: PreToolUse has the same gap, and the 2.1.263 bundle explains why every non-SubagentStop event does.

    Live observation (2.1.239, Linux) — a PreToolUse hook firing inside a Task-tool subagent receives the parent session's transcript_path (verified against the parent's jsonl on disk; no /subagents/ segment — the subagent's own transcript is a flat ~/.claude/projects/<slug>/<agent-session-uuid>.jsonl file whose internal sessionId equals its filename). No agent_id / agent_type / agent_transcript_path on the payload. We run a read-before-edit evidence gate off PreToolUse; for subagent-issued edits it can only sample the parent transcript, so it deterministically false-denies (reproduced mechanically for every executor subagent we dispatch).

    Why the remaining base fields can't compensate (2.1.263 bundle inspection) — all hook events share a base schema {session_id, transcript_path, cwd, prompt_id?}; PreToolUse only adds tool_name / tool_input / tool_use_id, and the agent triple exists only on SubagentStop's schema. Since transcript files are derived as projects/<slug>/<sessionId>.jsonl, a session_id pointing at the subagent would make transcript_path point at the subagent's file too — the payload constructor overwrites both from a single session context (it already contains a remap helper g6r(e, t) that sets agent_transcript_path = transcript_path whenever the key is present). So no per-agent signal is reachable from the current base. This generalizes the §1 ask: carry agent_id / agent_type / agent_transcript_path (or a boolean is_subagent) on the shared base rather than per-event.

    Minor datapoint for tracking: the 2.1.263 bundle already ships the remap helper above plus a deviceLaunchDir re-resolution path, so this surface appears to be actively evolving — we re-probe on each release and will report here if PreToolUse starts carrying the fields.

  9. Jackw2248 commented on Oct 19, 2026

    @Jackw2248

    Still present on 2.1.301 (macOS 14.4.1, Apple silicon, VS Code extension native binary), measured 2026-10-19 in a throwaway claude -p session with --setting-sources project and logging-only hooks. The 2.1.300 bundle builds these inputs the same way. I haven't run 2.1.302 or 2.1.303, and neither changelog mentions a fix.

    Repro (one run, about $1.20 on Sonnet):

    1. Project .claude/settings.json registering PreCompact, SessionStart, PreToolUse (matcher Read), SubagentStart and SubagentStop hooks. Each one runs cat > cap/$(date +%s)-$$-<Event>.json (create cap/ first).
    2. A data/ folder holding eight 68 KB text files (1,000 lines each).
    3. Run:
      CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=20 claude -p "Use the Agent tool to launch ONE general-purpose subagent with this task: read every file in data/ in full, one file per Read call, then reply with the last line of each file. Do not read the files yourself. Then reply DONE." --setting-sources project --model sonnet --output-format json --allowedTools Read,Glob,Agent,Task --disallowedTools Bash,Write,Edit,WebFetch,WebSearch --max-budget-usd 3

    The subagent auto-compacted: its own transcript has the compact_boundary (with agentId set), and the main transcript has none. The main session was idle at the time.

    Captured stdin (ids and paths shortened). The subagent's tool events identify it:

    {"session_id":"2064160d-…","transcript_path":"…/2064160d-….jsonl","cwd":"…","prompt_id":"e028d243-…","permission_mode":"default","agent_id":"afafa1c5197de17ac","agent_type":"general-purpose","effort":{"level":"medium"},"hook_event_name":"PreToolUse","tool_name":"Read", …}

    The same subagent's compaction, right after its last Read, carries none of agent_id, agent_type, permission_mode or effort:

    {"session_id":"2064160d-…","transcript_path":"…/2064160d-….jsonl","cwd":"…","prompt_id":"e028d243-…","hook_event_name":"PreCompact","trigger":"auto","custom_instructions":null}
    {"session_id":"2064160d-…","transcript_path":"…/2064160d-….jsonl","cwd":"…","prompt_id":"e028d243-…","hook_event_name":"SessionStart","source":"compact","model":"claude-sonnet-5-5"}

    §3 also still reproduces. A SubagentStop fired for the compaction summarizer with agent_type: "" and an agent_transcript_path that was never written.

    The overlap case. When the main session and a background subagent compact at about the same time, Claude Code fires two SessionStart(compact) events, and nothing in their input says which one is the main session's. A hook that re-injects resume state has to guess. We hit this: a subagent compacted about 30 s before the main session, its SessionStart(compact) received the main session's resume state, and our hook, taking that event for the main session's, did not re-inject the state at the main session's own compaction. Start order doesn't settle it either: the compaction above took 17 s, so one that starts first can finish last. The 2.1.302 change that always runs subagents in the background in VS Code and the desktop app should make this overlap more common. agent_id on these events would remove the guess.

    Unchanged in 2.1.301 (and 2.1.300): PreCompact, PostCompact and SessionStart call the shared input builder with only its first two arguments, so the context that carries agentId/agentType never reaches it. Tool events pass it as the fourth argument, which is why the PreToolUse above has both fields.

  10. JhouCode commented on Oct 23, 2026

    @JhouCode

    Field data on the consequence of item 1, from a setup that runs resume hooks on SessionStart:compact every day. The code reads above explain why the payload has no agent identity; this is what that does to hooks in real use, and the rule that kept it from costing me state.

    Where the output lands. Measured over the transcripts on one machine (linux-x64, 848 subagent transcripts):

    • 97 subagent auto-compactions, in 81 subagent transcripts;
    • in 81 of 81, the SessionStart:compact output landed inside the subagent's transcript;
    • in 96 of 97, the main transcript got nothing within two minutes of it (the one exception is a main compaction at the same moment);
    • negative control: the 767 subagent transcripts with no compaction carry 0 SessionStart attachments.

    So the text goes to the agent that compacted. What goes wrong is everything the hook decides from its payload, because the payload is the main session's: session_id and transcript_path both point to the main transcript.

    What that cost here, once. I had a hook on SessionStart:compact that returns the user turns a compaction swallowed (devolver): it reads a ledger keyed by transcript_path, emits the pending turns, and marks them returned. On 2026-09-25 a subagent compacted; the hook received the main transcript's path, found the main session's pending record, emitted its two user turns into the subagent, and marked them returned. The main session never got them. Nothing failed and nothing was logged as wrong: the record says returned.

    The rule that closes it: anything that consumes or writes state does not run on a compaction event. It runs on UserPromptSubmit, which only the main loop fires: 3,848 attachments in main transcripts, 0 in the 848 subagent transcripts. That hook now returns the turns on the first prompt after the compaction, and it can only ever see the main session. On SessionStart:compact I keep only output that is read-only and harmless in either agent: the always-on rules, which a subagent should have anyway, and a printout of the current state file (the baton). That layer is not small: a median of ~39k characters lands in the subagent per compaction, split into slices under the 10,000-unit hook cap by mapa-hook.sh. It is the price of the subagent keeping the rules, and it is paid only when it compacts.

    Why my subagents compact at all: the work is split by role. Inside the app I divide the work into sessions and subagents with one job each (who does what):

    • a desk session thinks and designs. A skill loads the open designs when it starts, and it ends in a self-contained brief. It does not operate;
    • a worker session launches the builder subagent (engenheiro, on Sonnet) with that brief verbatim, and a separate gate agent, with its own context, reads the logs and returns a verdict as N of M;
    • reviewers (code, guards, the math behind an experiment) are read-only subagents on Opus;
    • for video, a director subagent on Fable writes the shot list before the render and gives a verdict per frame after it; a Sonnet worker assembles and renders, and an art reviewer judges the finish against the last approved piece.

    That split is what makes the measurement above matter. Of the 97 subagent compactions, 72 were the builder (58 of its 314 transcripts), 24 general-purpose, 1 the director, and 0 any reviewer. The builders run long missions, so they are the ones that compact, and they are the ones that must still follow the rules afterwards. That is why the rules ride SessionStart:compact, and nothing that consumes state does.

    The cheapest isolation is no hooks at all. A curation pipeline here, which turns recorded calls into training examples, runs every stage as a headless claude -p with an empty HOME, --tools '' and --strict-mcp-config: no settings, no hooks, nothing to misfire. The model is picked per stage. The stage whose output becomes the truth the next one copies, the stage that writes the artifact and the judge stay on Sonnet; triage, parallel extraction and audit go to Haiku; one repair pass runs at the end, with the same judge re-judging. Cost per approved example went from $0.50 to about $0.17, with 67 of 113 approved over 30 videos.

    This does not replace the fix asked for here. agent_id on these three events would let a hook skip a subagent's compaction outright. Until then, the event choice is the only isolation a hook has.

    @mrinaldhillon, if you want this working before the fix lands: hipocampo, an open (MIT) arrangement of native Claude Code hooks, already does what your hooks need without the isSidechain heuristic. Each piece is documented with the measurement that put it there:

    • resume state injected into the subagent (your item 1): the part that consumes state runs on UserPromptSubmit, which a subagent never fires (devolver). SessionStart:compact carries only the rules and a read-only printout of the state, which are right in either agent (the wiring);
    • checkpointing on a compaction event (items 1 and 2): the writer does not take the event as proof. It keys each record to a compaction boundary it finds in the main transcript, so a subagent's compaction, which leaves no boundary there, cannot create a record (sincroniza);
    • the rules surviving every compaction, subagents included: the always-on layer goes in labelled slices under the cap (mapa-hook.sh).

    Start at step zero of the README. docs/CONTEXTO.md §7 has what reaches the model after a compaction, channel by channel.

  11. quillrun-agent commented on Oct 24, 2026

    @quillrun-agent

    Still present on 2.1.309 (macOS).

    Setup: custom agent overriding general-purpose with autoCompactWindow: 250000, plus a 100000-window probe agent for testing.

    • In the subagent, PostToolUse carries agent_id. PreCompact and SessionStart(compact) don't.
    • Captured SessionStart(compact) stdin inside the subagent: session_id and transcript_path of the parent, cwd, scratchpad_dir, prompt_id, source: "compact", model, session_title. No agent_id / agent_type.
    • Captured PreCompact stdin inside the subagent: session_id and transcript_path of the parent, cwd, scratchpad_dir, prompt_id, trigger: "auto", custom_instructions: null.
    • SessionStart(compact) output landed in the subagent's transcript after all 8 compactions we checked across 3 probes.
    • Timing isn't a usable discriminator either: in 117 of 125 main-loop compactions, the SessionStart(compact) hook started before the compact_boundary was written, while in subagents it started 0.00–0.29 s after.

    Current workaround: the hook output opens with a "main session only, subagents ignore this block" line. It works in a test, but only the model's compliance enforces it, so an agent_id on these events is still needed.

  12. jgh736 commented on Oct 26, 2026

    @jgh736

    Still present on 2.1.310 (Linux, native binary), measured 2026-10-27 in throwaway claude -p sessions on Haiku with a logging hook on every event. Same shape as the 2.1.301 and 2.1.309 reports above. One addition: forks have the same gap as ordinary subagents.

    • Ordinary subagent (custom --agents definition): 2 auto-compactions. PreCompact, PostCompact and SessionStart(compact) had the parent's session_id and transcript_path, and no agent_id or agent_type. The compact_boundary entries went only to subagents/agent-*.jsonl. The SessionStart(compact) output landed in the subagent's transcript.
    • Fork (Agent tool, subagent_type: "fork"): 3 auto-compactions with the same result. SubagentStart and the fork's PostToolUse carried agent_id and agent_type: "fork"; the three compaction events did not.
    • Main-loop and worker compaction inputs were field-identical apart from timestamps (session_id, prompt_id, cwd, model, trigger).
    • Timing confirms the 2026-09-24 and 2026-10-24 comments: a main-loop SessionStart(compact) ran ~35 ms before its compact_boundary reached the main transcript, and in a run where a background subagent and the main loop compacted close together, a guard that looked for a fresh boundary under subagents/ missed both subagent compactions. A transcript-based discriminator is not reliable.

    Our mitigation is the same as the 2.1.309 comment: the hand-back text tells a subagent or fork to ignore it. agent_id on these three events is still the fix.

    Written and posted by Claude (Claude Code, Opus 5.5) on behalf of the account owner.

  13. vs-av commented on Oct 27, 2026

    @vs-av

    Still present on 2.1.311 (macOS, Darwin 25.6, native install), measured 2026-10-27 in a throwaway claude -p session with a logging-only plugin hook on PreCompact and SessionStart (matcher compact), CLAUDE_CODE_AUTO_COMPACT_WINDOW=100000 and CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50.

    • One general-purpose subagent compacted 3 times (3 compact_boundary records with isSidechain: true in its subagents/agent-*.jsonl); the main session compacted 4 times.
    • All 7 PreCompact and 7 SessionStart(compact) inputs have the same keys: session_id, transcript_path, cwd, prompt_id, hook_event_name, plus trigger/custom_instructions or source/model. No agent_id, no agent_type.
    • In the subagent's 6 inputs, session_id and transcript_path are the parent's (the main transcript, not the subagent file), so a hook cannot tell the two apart from stdin.
    • Workaround we're using until this is fixed: treat the event as a subagent's when a subagents/*.jsonl file under the session folder was written within ~1 s of the hook (the gap was ~0.2 s in all 3 cases), and read agent_id/agent_type first so the hook picks them up once they're sent.
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions