Skip to content

Session state sticks at "working" after Claude Code emits a session recap #199

@dhilgaertner

Description

@dhilgaertner

Summary

Claude Code added a session recap feature (shipped in v2.1.108 on 2026-04-14, controlled by the awaySummaryEnabled setting) that prints a one-line summary at the end of a turn, for example:

✻ Brewed for 35s

※ recap: Adding a GitHub Actions workflow to push Corveil images to commercial ECR on
  merge to main (ticket #324); PR #326 is open and ready for review. Next: merge it,
  then run the post-merge ECR verification. (disable recaps in /config)

After this prints, Crow's session indicator stays on working and never transitions back to done, even though the agent is idle and ready for input.

Why this matters

The whole value of the status indicator is knowing when a session is done so I can pick up the next one. With the recap feature on (which is the default for recent Claude Code versions), every session I finish looks like it's still running. I end up force-checking each one manually.

Root cause — not terminal parsing, but hook events

Crow does not grep terminal output for or ※ recap: — it transitions state purely from Claude Code hook events via the hook-event RPC handler:

  • Sources/Crow/App/AppDelegate.swift:802-968 — hook-event dispatcher
  • Packages/CrowCore/Sources/CrowCore/Models/Enums.swift:33-38ClaudeState { idle, working, waiting, done }
  • AppDelegate.swift:872PreToolUse.working
  • AppDelegate.swift:930-931UserPromptSubmit.working
  • AppDelegate.swift:933-935Stop.done

So the sequence for a normal turn is: UserPromptSubmit.working → (tool calls) → Stop.done.

The hypothesis: when the recap generates, Claude Code re-enters a generation cycle after the turn has already emitted Stop. That secondary cycle likely fires one of UserPromptSubmit / PreToolUse / SubagentStart, which flips us back to .working — but the closing event for that cycle is either missing, uses a different event name (e.g. SubagentStop not mapped to .done), or is swallowed, so we never transition back.

This needs to be confirmed by capturing the actual hook-event stream around a recap. Possible outcomes:

  1. Recap runs as a subagent: fires SubagentStart.working, then SubagentStop which is not currently mapped back to .done at line 933-935.
  2. Recap fires a UserPromptSubmit (synthetic, internal prompt): flips to .working, then completes with a Stop that arrives but is missed because of ordering (e.g. two Stops collapse, or state is clobbered by a later event).
  3. Recap doesn't emit any hook events at all and the .working state we're seeing is left over from something else the session did right before the recap rendered.

Option 1 is the most likely fit — the recap is essentially a short summarization task, and Claude Code's recap docs describe it as running in the background.

Reproduction

  1. Use Claude Code ≥ v2.1.108 (recap defaults to on).
  2. Start a Crow session on any ticket that takes more than a couple of turns to complete.
  3. Let the agent finish naturally (it will print a ※ recap: ... (disable recaps in /config) line at the end).
  4. Observe that the Crow sidebar keeps showing the session as working indefinitely.

Suggested fix

Investigate first, then implement:

  1. Instrument AppDelegate.swift hook-event handler to log every inbound event name + source/session ID for a single reproduction. Capture what actually fires around a recap.
  2. Based on what we see, either:
    • Add a mapping for SubagentStop (or whatever closing event the recap uses) → .done in the Stop/StopFailure block at AppDelegate.swift:933-938.
    • Or: tag events originating from the recap generator (if Claude Code identifies them via a source, agentType, or similar field on the hook payload) and ignore them for state-transition purposes so the state stays .done across the recap.

Do NOT try to detect the recap by pattern-matching terminal text for ※ recap: — it sidesteps the actual hook-lifecycle bug and will rot the next time the recap format changes.

Workaround for users in the meantime

Users can disable the feature in one of three ways:

  • settings.json: \"awaySummaryEnabled\": false
  • /config inside Claude Code → toggle "Session recap" off
  • Env: CLAUDE_CODE_ENABLE_AWAY_SUMMARY=0

Worth calling out in the README or the "Known issues" note once we know the exact behavior — but the real fix belongs in the hook-event handler.

References

  • Claude Code changelog entry for v2.1.108 (2026-04-14): session recap feature.
  • Claude Code interactive-mode docs: describe recap as background-generated and invokable via /recap.
  • Claude Code settings docs: awaySummaryEnabled.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions