Skip to content

perf: optimize stop hook - transcript parsing and tree updates#407

Open
yasunogithub wants to merge 3 commits intoentireio:mainfrom
yasunogithub:perf/stop-hook-optimization
Open

perf: optimize stop hook - transcript parsing and tree updates#407
yasunogithub wants to merge 3 commits intoentireio:mainfrom
yasunogithub:perf/stop-hook-optimization

Conversation

@yasunogithub
Copy link

Summary

  • Transcript parsing 3x → 1x: commitWithMetadata() was parsing the same transcript file three times (for prompt extraction, modified file detection, and token usage calculation). Added FromLines variants that accept pre-parsed []TranscriptLine, eliminating redundant file I/O and JSON parsing.

  • Incremental git tree updates: buildTreeWithChanges() and addTaskMetadataToTree() previously flattened the entire git tree (O(N) total files) then rebuilt from scratch. Replaced with applyChangesToTree() which traverses only directories containing changes — unchanged subtrees are reused by hash. Complexity: O(changed_files × tree_depth).

  • Hook binary fallback: Switched .claude/settings.json from go run (recompiles every invocation ~1s) to pre-built binary with helpful error if missing.

Test plan

  • mise run fmt — no formatting issues
  • mise run lint — 0 issues
  • mise run test:ci — all unit + integration tests pass
  • Manual: restart Claude Code session and verify hooks execute faster
  • Manual: time ./entire hooks claude-code stop to measure binary execution time

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 18, 2026 09:56
@yasunogithub yasunogithub requested a review from a team as a code owner February 18, 2026 09:56
@yasunogithub yasunogithub force-pushed the perf/stop-hook-optimization branch from aa9a6dd to 30b8bc0 Compare February 18, 2026 10:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes Claude Code stop-hook performance in Entire CLI by avoiding redundant transcript parsing and by updating checkpoint tree construction to reuse unchanged git subtrees (incremental tree updates). It also tweaks the repo’s Claude hook configuration to prefer a prebuilt entire binary over go run.

Changes:

  • Add FromLines APIs to reuse pre-parsed transcript lines for modified-file detection and token usage.
  • Replace flatten/rebuild checkpoint tree logic with an incremental tree update implementation.
  • Update .claude/settings.json hook commands to run a prebuilt ./entire binary with a fallback message.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/entire/cli/hooks_claudecode_handlers.go Reuses already-parsed transcript lines for modified-file extraction and (sometimes) token usage.
cmd/entire/cli/checkpoint/tree_incremental.go New incremental git tree update implementation (changeTree, applyChangesToTree).
cmd/entire/cli/checkpoint/temporary.go Switches checkpoint tree creation / metadata injection to incremental updates.
cmd/entire/cli/agent/claudecode/transcript.go Adds CalculateTotalTokenUsageFromLines and ExtractAllModifiedFilesFromLines.
.claude/settings.json Uses prebuilt binary instead of go run for Claude hooks.

Comment on lines +88 to +92
for _, entry := range baseTree.Entries {
if entry.Mode == filemode.Dir {
if dirChanges, ok := changes.dirs[entry.Name]; ok {
// Recurse into subdirectory with changes
newSubHash, subErr := applyChangesToTree(repo, entry.Hash, dirChanges)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyChangesToTree() doesn’t handle file/dir type collisions (e.g., base has a directory named X but changes adds a file X, or vice versa). In that case the existing entry is kept and the new one is added later, producing duplicate tree entries for the same name (invalid tree). Consider explicitly detecting name conflicts between changes.files and changes.dirs while iterating baseTree.Entries and either replacing the existing entry (drop the old one) or returning an error when both a file and directory change exist for the same name.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added type conflict handling: when base has a dir but changes has a file (or vice versa), the entry is now replaced instead of creating duplicates.

Comment on lines 136 to 149
// Add new directories (not in base tree)
for name, dirChanges := range changes.dirs {
if !processedDirs[name] {
newSubHash, subErr := createTreeFromChanges(repo, dirChanges)
if subErr != nil {
return plumbing.ZeroHash, subErr
}
newEntries = append(newEntries, object.TreeEntry{
Name: name,
Mode: filemode.Dir,
Hash: newSubHash,
})
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a path is marked for deletion but the directory doesn’t exist in the base tree, the “Add new directories” block will still create the directory via createTreeFromChanges(), even if that subtree contains only deletions and ends up empty. This can introduce empty directory entries into commits, which Git normally doesn’t track. Recommend skipping creation of new directory entries when the resulting subtree has no entries (and/or avoid recording delete-only paths for non-existent base paths).

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. createTreeFromChanges now returns ZeroHash when all entries are deletions, and callers skip adding empty directory entries.

Comment on lines 221 to 223
// Use forward slashes for git tree paths
treePath := dirPathRel + "/" + filepath.ToSlash(relWithinDir)
ct.addFile(treePath, blobHash, mode)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addDirectoryToChangeTree() documents that ct.addFile() paths must use forward slashes, but treePath is built from dirPathRel without normalizing it. Callers sometimes build MetadataDir with filepath.Join (which uses OS separators), so on Windows this can create directory names containing backslashes and break tree layout. Normalize dirPathRel (and/or the full path in ct.addFile/deleteFile) with filepath.ToSlash before splitting/recording paths.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. dirPathRel is now normalized with filepath.ToSlash() before use.

Copy link
Collaborator

@Soph Soph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yasunogithub thanks a lot for this! The changes look good, I'd love to have more tests for tree_incremental.go. Would you be up to add them? Or would you be ok me taking over this PR and add more tests (and run it against our e2e suite).

@Soph Soph added enhancement New feature or request labels Feb 18, 2026
@yasunogithub
Copy link
Author

@yasunogithub thanks a lot for this! The changes look good, I'd love to have more tests for tree_incremental.go. Would you be up to add them? Or would you be ok me taking over this PR and add more tests (and run it against our e2e suite).

I’m totally fine with you taking over the PR to add more tests and run the e2e suite. Thanks a lot!

yasunogithub and others added 3 commits February 19, 2026 15:42
…ree updates

Two main optimizations to reduce stop hook latency:

1. Transcript parsing 3x → 1x: commitWithMetadata() was parsing the transcript
   file three separate times (prompt extraction, modified files, token usage).
   Added FromLines variants (ExtractAllModifiedFilesFromLines,
   CalculateTotalTokenUsageFromLines) that accept pre-parsed lines, eliminating
   redundant file I/O and JSON parsing.

2. Incremental git tree updates: buildTreeWithChanges() and addTaskMetadataToTree()
   previously flattened the entire git tree into a map (O(N) where N=total files),
   modified entries, then rebuilt from scratch. Replaced with applyChangesToTree()
   which only traverses directories containing changes - unchanged subtrees are
   reused by hash reference. Complexity is now O(M*D) where M=changed files and
   D=tree depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: ce934f044ff2
- Remove waitForTranscriptFlush (0-3s savings per stop hook call)
- Use PrePromptState.UserPrompt instead of parsing transcript for commit messages
- Use git status (DetectFileChanges) instead of transcript for modified file detection
- Remove extractLastAssistantMessage, extractKeyActions, createContextFileMinimal (dead code)
- Add transcript.CountLines for fast line counting without JSON parsing
- Keep lightweight transcript file copy for rewind support
- Fallback to transcript parsing only when UserPrompt unavailable (backward compat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 54b3d488ad4d
- Handle file/dir type conflicts in applyChangesToTree (dir→file and file→dir)
- Skip empty trees from delete-only changes in createTreeFromChanges
- Normalize dirPathRel with filepath.ToSlash for Windows compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 6dc290b19fdd
@yasunogithub yasunogithub force-pushed the perf/stop-hook-optimization branch from 015fb69 to 91e941d Compare February 19, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

2 participants

Comments