fix(skills): clear workspaceVersions entry when skills watcher is disabled#77998
Conversation
|
Codex review: needs maintainer review before merge. Reviewed May 31, 2026, 2:32 PM ET / 18:32 UTC. Summary PR surface: Source +43, Tests +94. Total +137 across 3 files. Reproducibility: yes. source-reproducible: current main keeps Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Keep this focused runtime cleanup and land it after exact-head checks, with watcher lifecycle ownership staying in Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main keeps Is this the best way to solve the issue? Yes. The PR fixes the owner runtime path, preserves monotonic snapshot invalidation, and adds focused tests; a session-end hook would be tighter, but the established current entry point is per-turn AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 01ef169004bc. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +43, Tests +94. Total +137 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
376a9e0 to
3434053
Compare
|
Rebased onto current @vincentkoc — flagging for review when convenient. |
|
This pull request has been automatically marked as stale due to inactivity. |
…abled refresh-state.ts kept a module-level Map<workspaceDir, number> populated on every skills file event, but had no production removal path. The sibling watchers Map in refresh.ts cleans up on watcher teardown (watchEnabled=false config change), but workspaceVersions was forgotten. Add clearSkillsSnapshotVersionForWorkspace and call it from the existing watcher-disable teardown path so workspaceVersions follows watchers' lifecycle. Removal is safe: subsequent getSkillsSnapshotVersion reads fall back to globalVersion, and shouldRefreshSnapshotForVersion forces a refresh whenever a consumer was holding a non-zero cached version. Note: this is the lifecycle-gap fix on the existing teardown path; the broader fact that ensureSkillsWatcher itself never tears down on session-end is a separate concern not addressed here.
Add an opportunistic idle sweep to ensureSkillsWatcher: every call walks watchers and disposes those with lastEnsuredAt < now - 1h, closing the chokidar instance, clearing the debounce timer, and clearing the matching workspaceVersions entry. Sessions re-call ensureSkillsWatcher on every turn, so any workspace with an active session refreshes its lastEnsuredAt and stays attached. Only abandoned watchers (workspaces no longer touched by any session for \>=1h) get torn down. This bounds watchers.size to the working set instead of growing one entry per distinct workspaceDir over the gateway lifetime. The previous narrow fix on the watchEnabled=false teardown path is also unified through the new disposeSkillsWatcher helper so version-entry cleanup is consistent across all teardown sites.
3434053 to
67bd2df
Compare
|
Land-ready after maintainer rewrite/rebase. What changed:
Verification:
Known proof gap: the live E2E simulates the 1-hour TTL clock edge with |
Closes #77997
Summary
Bounds skills watcher and snapshot-version state to the active workspace set on the current
src/skills/runtimeimplementation.clearSkillsSnapshotVersionForWorkspace()and uses it from watcher teardown while preserving pending invalidation by promoting the workspace version intoglobalVersionbefore deleting the workspace key.disposeWorkspaceWatchState()path for explicitwatch: falseteardown and idle eviction. Disposal unsubscribes the workspace from shared path watchers, clears target/cache/last-ensure state, marks the workspace dirty, then clears the per-workspace version key.CHANGELOG.mdedit from the PR; release generation owns changelog updates.Verification
node scripts/run-vitest.mjs src/skills/runtime/refresh.test.ts src/skills/runtime/session-snapshot.test.ts-> 34/34 pass.pnpm exec oxfmt --check --threads=1 src/skills/runtime/refresh.ts src/skills/runtime/refresh-state.ts src/skills/runtime/refresh.test.ts-> pass..agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main-> clean, no accepted/actionable findings.run_5057e6eaab4f, provideraws, leasecbx_4afdccef9561, commandenv OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed-> pass.run_512d4b6c1717, provideraws, leasecbx_4afdccef9561-> pass.67bd2df6c131ff12fc12ab65ebdc63e2c6898193-> green;gh pr checks 77998 --watch=falsereturned all relevant checks passing/skipped.Real behavior proof
workspaceVersionsretained workspace entries forever, and currentsrc/skills/runtime/refresh.tsretained per-workspace watch target/cache/subscriber state after sessions stopped touching a workspace. A long-lived gateway that sees many distinct workspaces can accumulate stale workspace snapshot-version keys and shared watcher subscriber bookkeeping.run_512d4b6c1717) on Linux with Node 24.15.0. The proof used real temp workspaces, realchokidarwatchers, realSKILL.mdwrites, and the public runtime APIs fromsrc/skills/runtime/refresh.ts.tsxscript via Crabbox that created temp workspaces, calledensureSkillsWatcher(), wroteSKILL.mdfiles, waited for real watch events, disabled watching, simulated idle TTL by controllingDate.now()aroundensureSkillsWatcher(), re-enabled a workspace, and checkedgetSkillsSnapshotVersion()/shouldRefreshSnapshotForVersion()after each lifecycle transition.{"result":"PASS","watchedVersion":1780252438684,"disabledVersion":1780252438735,"followupVersion":1780252438736,"idleVersion":1780252440024,"evictedVersion":1780256040087,"reenabledVersion":1780256040087}Date.now()so the lifecycle path could be exercised without a one-hour test.