Skip to content

perf(core): cache skills loading with fsnotify invalidation - #121

Merged
omarluq merged 4 commits into
mainfrom
feat/skills-cache
Jun 14, 2026
Merged

perf(core): cache skills loading with fsnotify invalidation#121
omarluq merged 4 commits into
mainfrom
feat/skills-cache

Conversation

@omarluq

@omarluq omarluq commented Jun 14, 2026

Copy link
Copy Markdown
Owner

LoadSkills was called on every prompt, walking up to 4 directory trees,
reading every SKILL.md, and parsing YAML frontmatter. With 100+ skills
this adds 10-50ms of filesystem I/O per prompt. Add a SkillsCache backed
by samber/hot (W-TinyLFU, 1h TTL) with a background fsnotify watcher
that debounces burst events (500ms) before purging. Cache misses are
deduplicated via singleflight. Cache is CWD-scoped, lifecycle-managed
through a SkillsService DI registration, and gracefully degrades to
direct disk loads if fsnotify is unavailable or after Close. - Add
internal/core/skills_cache.go and tests (7 cases) - Add
internal/di/skills_service.go with Shutdown lifecycle - Wire SkillsCache
into Runtime.loadSkills() used by context_build,
context_auto_compaction, and slash commands

LoadSkills was called on every prompt, walking up to 4 directory trees,
reading every SKILL.md, and parsing YAML frontmatter. With 100+ skills
this adds 10-50ms of filesystem I/O per prompt. Add a SkillsCache backed
by samber/hot (W-TinyLFU, 1h TTL) with a background fsnotify watcher
that debounces burst events (500ms) before purging. Cache misses are
deduplicated via singleflight. Cache is CWD-scoped, lifecycle-managed
through a SkillsService DI registration, and gracefully degrades to
direct disk loads if fsnotify is unavailable or after Close. - Add
internal/core/skills_cache.go and tests (7 cases) - Add
internal/di/skills_service.go with Shutdown lifecycle - Wire SkillsCache
into Runtime.loadSkills() used by context_build,
context_auto_compaction, and slash commands
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c1bba72-7e43-45a8-aa00-364a2b98ab8b

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1bd4e and 56ed805.

📒 Files selected for processing (1)
  • internal/assistant/testing.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/assistant/testing.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Added skills caching with filesystem monitoring to keep skill data fresh and improve performance.
  • Bug Fixes
    • Ensures assistant context building and command handling use the updated skills source, reflecting changes on disk when they occur.
  • Chores
    • Refactored internal test runtime setup to standardize how test dependencies are initialized across the suite.
    • Added dedicated test utilities and coverage for skills cache behavior and lifecycle.

Walkthrough

Introduces SkillsCache in internal/core that memoizes LoadSkills results per CWD using a bounded TTL cache with optional fsnotify-based invalidation and debounced purge. Adds test infrastructure helpers (RuntimeTestOptions, NewRuntimeForTest, runtimeDeps, newRuntimeFromDeps) to centralize test runtime construction. Creates a SkillsService DI wrapper, registers it, and wires its cache into assistant.Runtime via a new SkillsCache field in RuntimeOptions. All internal skill-loading call sites switch to a new loadSkills helper that prefers the cache. Refactors test helpers and individual tests across the codebase to use the new infrastructure.

Changes

Skills Cache, Test Infrastructure, DI Wiring, and Runtime Integration

Layer / File(s) Summary
SkillsCache core implementation
internal/core/skills_cache.go
New SkillsCache struct wrapping hot.HotCache with TTL, optional fsnotify watcher, debounced Purge() (500ms), and Get/Close API. Includes watchSkillDirs, addWatch, schedulePurge, cancelPurge, and nearestExistingDir helpers for filesystem monitoring and graceful degradation.
SkillsCache test suite
internal/core/skills_cache_test.go
Comprehensive test suite validating cache initialization, Get behavior on empty CWD, skill loading and caching, consistent results across calls, parity with direct LoadSkills, idempotent Close, safety after Close, CWD isolation, filesystem invalidation on file changes, late directory creation, and debounced burst writes.
Test infrastructure helpers
internal/assistant/testing.go, internal/assistant/provider_hook_test_helpers_internal_test.go
RuntimeTestOptions struct and NewRuntimeForTest factory for high-level test runtime setup with optional dependency callback. runtimeDeps struct and newRuntimeFromDeps helper for low-level internal test wiring with explicit field defaults and automatic HTTP client setup.
SkillsService DI integration
internal/di/skills_service.go, internal/di/register.go, internal/di/container.go, internal/di/assistant_service.go, internal/di/assistant_service_internal_test.go
SkillsService wraps core.SkillsCache with Shutdown method. Registered via do.Provide in RegisterServices. Exposed through Container.SkillsService() accessor. NewAssistantService resolves the service and wires skills.Cache into assistant.RuntimeOptions.SkillsCache. Test setup provisions and cleans up the cache.
Runtime cache integration
internal/assistant/runtime.go
Runtime struct gains skillsCache field; RuntimeOptions gains SkillsCache *core.SkillsCache field. NewRuntime wires the option into the runtime. New loadSkills(cwd) helper returns cached skills when available or falls back to core.LoadSkills.
Functional call-site updates
internal/assistant/context_build.go, internal/assistant/runtime_slash.go
ContextUsage and modelContextBase in context_build.go switch to runtime.loadSkills(cwd) for determining skills used in prompt estimation and auto-activation. respondToSkillCommand in runtime_slash.go loads skills via runtime.loadSkills(cwd).
Test migration to new infrastructure
internal/assistant/*_test.go, internal/terminal/*_internal_test.go
All test helpers and individual tests refactored to use NewRuntimeForTest (with RuntimeTestOptions callback) or newRuntimeFromDeps for runtime construction, replacing inline RuntimeOptions/Runtime struct literals and reducing boilerplate null assignments.

Sequence Diagram

sequenceDiagram
  participant AssistantRuntime
  participant loadSkills
  participant SkillsCache
  participant HotCache
  participant core_LoadSkills as core.LoadSkills

  AssistantRuntime->>loadSkills: loadSkills(cwd)
  alt skillsCache is set
    loadSkills->>SkillsCache: Get(cwd)
    SkillsCache->>HotCache: fetch(cwd)
    alt cache hit
      HotCache-->>SkillsCache: cached LoadSkillsResult
    else cache miss
      HotCache->>core_LoadSkills: LoadSkills(cwd)
      core_LoadSkills-->>HotCache: LoadSkillsResult
      HotCache->>SkillsCache: register watches via watchSkillDirs
    end
    SkillsCache-->>loadSkills: LoadSkillsResult.Skills
  else skillsCache is nil (test or fallback)
    loadSkills->>core_LoadSkills: LoadSkills(cwd, nil, true)
    core_LoadSkills-->>loadSkills: LoadSkillsResult.Skills
  end
  loadSkills-->>AssistantRuntime: []Skill
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • omarluq/librecode#63: Both PRs touch the assistant's runtime skill-loading flow—main adds runtime.loadSkills(cwd) (and wires skills cache) then switches context_build/runtime_slash.go to use it, while the retrieved PR refactors runtime behavior into separate runtime_* files including runtime_slash where the skill-loading logic resides.

Poem

🐰 A cache for my skills, what a hop-tastic feat,
No more disk trips for every repeat!
The watcher stands guard with a fsnotify ear,
Debouncing the bursts so results stay clear.
Test helpers refactored with care and delight—
Now skills load with speed, from dawn until night! ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately describes the main change: implementing a caching layer for skills loading with filesystem change invalidation via fsnotify.
Description check ✅ Passed The description provides clear context about the performance problem being solved and comprehensive technical details about the caching implementation strategy, scope, and lifecycle management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skills-cache

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.01887% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.41%. Comparing base (c8a4381) to head (56ed805).

Files with missing lines Patch % Lines
internal/core/skills_cache.go 83.00% 10 Missing and 7 partials ⚠️
internal/di/skills_service.go 0.00% 6 Missing ⚠️
internal/assistant/runtime.go 84.61% 1 Missing and 1 partial ⚠️
internal/di/container.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #121      +/-   ##
==========================================
+ Coverage   78.40%   78.41%   +0.01%     
==========================================
  Files         282      285       +3     
  Lines       22390    22530     +140     
==========================================
+ Hits        17555    17668     +113     
- Misses       3586     3605      +19     
- Partials     1249     1257       +8     
Flag Coverage Δ
unittests 78.41% <83.01%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/core/skills_cache.go`:
- Around line 182-191: The watch registration for default skill paths is skipped
when directories don't exist initially, causing cache invalidation to fail if
those directories are created later. In the loop that processes
defaultSkillPaths with filepath.Clean, remove the condition that skips watch
registration based on resourcePathExists, and instead register the watch for all
paths regardless of their current existence. Keep the check for already-watched
directories to avoid duplicates, but ensure that every path returned by
defaultSkillPaths gets added to the watched map and has c.addWatch called on it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40e82c9f-e417-4bd4-998c-17356d866d09

📥 Commits

Reviewing files that changed from the base of the PR and between c8a4381 and cf76aea.

📒 Files selected for processing (20)
  • internal/assistant/context_auto_compaction_internal_test.go
  • internal/assistant/context_auto_compaction_test.go
  • internal/assistant/context_budget_test.go
  • internal/assistant/context_build.go
  • internal/assistant/context_compaction_lifecycle_test.go
  • internal/assistant/context_compaction_test.go
  • internal/assistant/provider_hooks_internal_test.go
  • internal/assistant/runtime.go
  • internal/assistant/runtime_slash.go
  • internal/assistant/runtime_test.go
  • internal/assistant/tool_executor_internal_test.go
  • internal/core/skills_cache.go
  • internal/core/skills_cache_test.go
  • internal/di/assistant_service.go
  • internal/di/assistant_service_internal_test.go
  • internal/di/container.go
  • internal/di/register.go
  • internal/di/skills_service.go
  • internal/terminal/prompt_send_internal_test.go
  • internal/terminal/render_parity_internal_test.go

Comment thread internal/core/skills_cache.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/assistant/testing.go (1)

13-22: ⚡ Quick win

Expose SkillsCache in RuntimeTestOptions to keep test wiring aligned with RuntimeOptions.

NewRuntimeForTest currently hardcodes SkillsCache to nil, which prevents tests from explicitly exercising cache-enabled runtime paths. Add it to the helper options and forward it.

♻️ Proposed change
 import (
 	"log/slog"
 
 	"github.com/omarluq/librecode/internal/config"
+	"github.com/omarluq/librecode/internal/core"
 	"github.com/omarluq/librecode/internal/database"
 	"github.com/omarluq/librecode/internal/event"
 	"github.com/omarluq/librecode/internal/model"
 )
@@
 type RuntimeTestOptions struct {
 	Config     *config.Config
 	Sessions   *database.SessionRepository
 	Extensions runtimeExtensions
 	Cache      *ResponseCache
 	Events     *event.Bus
 	Models     *model.Registry
 	Client     Completer
 	Logger     *slog.Logger
+	SkillsCache *core.SkillsCache
 }
@@
 	return NewRuntime(&RuntimeOptions{
 		Config:      opts.Config,
 		Sessions:    opts.Sessions,
 		Extensions:  opts.Extensions,
 		Cache:       opts.Cache,
 		Events:      opts.Events,
 		Models:      opts.Models,
 		Client:      opts.Client,
 		Logger:      opts.Logger,
-		SkillsCache: nil,
+		SkillsCache: opts.SkillsCache,
 	})
 }

Also applies to: 43-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/assistant/testing.go` around lines 13 - 22, Add a SkillsCache field
to the RuntimeTestOptions struct (at lines 13-22) to match the runtime
configuration options. Then update the NewRuntimeForTest function (at lines
43-53) to accept the SkillsCache value from RuntimeTestOptions and forward it
when creating the runtime instance, instead of hardcoding it to nil, so that
tests can explicitly exercise cache-enabled runtime paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/assistant/testing.go`:
- Around line 13-22: Add a SkillsCache field to the RuntimeTestOptions struct
(at lines 13-22) to match the runtime configuration options. Then update the
NewRuntimeForTest function (at lines 43-53) to accept the SkillsCache value from
RuntimeTestOptions and forward it when creating the runtime instance, instead of
hardcoding it to nil, so that tests can explicitly exercise cache-enabled
runtime paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff98c89b-7559-4159-98c4-e78cce30408d

📥 Commits

Reviewing files that changed from the base of the PR and between cf76aea and 0e1bd4e.

📒 Files selected for processing (14)
  • internal/assistant/context_auto_compaction_internal_test.go
  • internal/assistant/context_auto_compaction_test.go
  • internal/assistant/context_budget_test.go
  • internal/assistant/context_compaction_lifecycle_test.go
  • internal/assistant/context_compaction_test.go
  • internal/assistant/provider_hook_test_helpers_internal_test.go
  • internal/assistant/provider_hooks_internal_test.go
  • internal/assistant/runtime_test.go
  • internal/assistant/testing.go
  • internal/assistant/tool_executor_internal_test.go
  • internal/core/skills_cache.go
  • internal/core/skills_cache_test.go
  • internal/terminal/prompt_send_internal_test.go
  • internal/terminal/render_parity_internal_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/core/skills_cache_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 14, 2026
@sonarqubecloud

Copy link
Copy Markdown

@omarluq
omarluq merged commit c5ab089 into main Jun 14, 2026
15 of 19 checks passed
@omarluq
omarluq deleted the feat/skills-cache branch June 14, 2026 16:48
@coderabbitai coderabbitai Bot mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant