Conversation
…ver reaches another team's stats Dashboard events already carry a cwd and are filtered by scope before a report, but skill-usage events do not, so ~/.teamai/usage.jsonl is aggregated whole and every project on the machine is shipped to whatever repo pull happens to be reporting to. Give UsageEvent an optional cwd, populate it from the hook payload via the existing resolveHookCwd helper (which already handles Cursor's workspace_roots), and filter usage with the same filterEventsByScope dashboard events use. That function is now generic over any event carrying a cwd, so both call sites share one set of path rules. Events written before this field have no cwd. A projectRoot report drops them, since they cannot be shown to belong to that project; excludeProjectRoots keeps them, since they cannot be shown to belong to an excluded one. pull reports to both scopes with complementary filters, so no event is lost from the local queue. Fixes Tencent#748 (bug 2). Bug 1 of that issue -- the Stop-hook nudge reaching projects that never initialized teamai -- is left to Tencent#747, which is already reworking the same gate.
|
Thanks for picking this up. #753 fixes the same bug 2 of #748 with a different design, so only one of the two can land. I ran one end-to-end script against a build of each. The script uses the real CLI with an isolated HOME. Project A reports to team-a, project C to team-c, and repo B has no TeamAI. A skill is used in A, then C pulls before A does.
This is how #750 loses the skill: The PR expects the user-scope target to report what the project target drops. A single So a user-scope team would still receive every project's skills. I read that from the code and did not run it. #753 drops the filter instead. Each scope writes its own Would you be OK closing this in favour of #753? If you know a case #753 misses, I'd like to add it there. |
|
Thanks for the thorough review -- and for running the end-to-end script against a build of each. I read the code paths you named and I agree on both counts. The lost event. My PR takes The user-scope hole. Closing in favour of #753 is fine by me. I'd rather have the bug actually fixed than my version merged, and the per-scope file also removes the class of bug rather than patching one instance of it. One note in case it's useful: the part of my diff I'd keep is the I also went looking for a gap in #753 before replying, in case there was a case worth adding there: I suspected the per-scope Thanks for the comparison table -- it made the decision easy. |
|
Closing in favour of #753, as discussed above. |
A scope that never reports (an http source, `usageReport: false`, a remote rejecting every push) never truncated its usage file, so it grew without bound. After the report step, pull now keeps each active scope's newest 5,000 events; a file at or below the cap is not rewritten. The cap runs after the report's truncate, never between its read and truncate, where it would shift the deleted lines onto unsent events (Tencent#750). Every writer of the usage file takes one lock beside it (acquireLock from update.ts with a bounded retry): hook appends wait at most ~250 ms and then write anyway, the truncate and the cap wait up to ~5 s. A rewrite therefore cannot drop an append made while it runs, and two pulls (http scopes included, which hold no sync lock) cannot interleave their rewrites. Both rewrites go through a uniquely named temp file beside the realpath'd file, created with its mode and renamed over it, so a kill or a full disk leaves the old file whole; the next locked rewrite removes temps a killed one left behind.
Summary
Skill-usage events are written by the same hooks as dashboard events, but only
dashboard events carry a
cwd.reportUsageToTeamtherefore filters one streamby scope and aggregates the other whole, so a machine holding several projects
ships every project's usage to whatever repo
pullhappens to be reporting to.This is bug 2 of #748. Bug 1 — the Stop-hook nudge reaching projects that never
initialized teamai — is left alone: #747 is already reworking
contributeHintAllowed, which is where that fix belongs.What changed
UsageEventgains an optionalcwd, populated at every hook write site throughthe existing
resolveHookCwdhelper (which already handles Cursor'sworkspace_rootsand treats an emptycwdas missing).filterEventsByScopebecomes generic over any event carrying a
cwd, so both streams share one set ofpath rules — including the Windows separator/case folding that the dashboard
tests already pin.
Write sites:
hook-handlers.tstrackhandlerresolveHookCwd(stdin)hook-handlers.tstrack-slashhandlerresolveHookCwd(stdin)usage-tracker.tstrackFromStdinresolveHookCwd(hookData)usage-tracker.tstrackSlashCommandresolveHookCwd(hookData)The legacy
teamai track <toolName> <toolInput>form takes only a tool-inputstring and has no hook payload to read a cwd from, so its events keep
cwdundefined. No production hook uses it — the dispatcher path is the one above.
Behavior for events without a cwd
Events written before this field, and events from a hook that ran without a
cwd, have no value. The two filters treat them asymmetrically, matching howdashboard events are already treated:
projectRootreport drops them — they cannot be shown to belong to that project;excludeProjectRootsreport keeps them — they cannot be shown to belong to an excluded one.pullreports to both scopes with complementary filters, so nothing is lost fromthe local queue: the union of the two targets covers every event, and
truncateUsageAfterReportstill counts the unfiltered file length.Test Plan
Unit tests
npx vitest runfor the touched suites:New coverage:
scope-filter.test.ts— afilterEventsByScope with usage events (#748)block mirroring the dashboard cases, including the Windows separator/case
rules.
team-push-interventions.test.ts— areportUsageToTeam — usage scope isolation (#748)block that seeds oneusage.jsonlwith events from twoprojects plus one legacy event and asserts the resulting
stats/<user>.yaml. Both directions are covered: aprojectRootreportdrops the other project's skills, an
excludeProjectRootsreport drops thisproject's, and a report with no scope option keeps everything (backward
compatible).
Before the fix the first of those failed with the other project's skill present
in the stats file — the leak itself, reproduced.
End-to-end, real CLI
Built with
npm run buildand ran the actual binary against an isolatedHOMEwith two project directories.
Claude-style payload (
cwd):Cursor-style payload (no
cwd,workspace_rootsinstead) — the resolver picksthe workspace root:
Slash-command path:
(
track-slashskips a name that is not on disk, so the skill had to exist under~/.claude/skills/before it recorded anything — that check predates thischange.)
Not verified
I did not exercise a real
teamai pullagainst a live git remote, and I did notrun
npm run test:e2e— it needs a live test repo I do not have here. The e2eabove covers the write side (the half this change adds); the read side is
covered by the unit tests, which drive the real
reportUsageToTeamand assertthe stats file it writes.
Windows: the Windows path rules are asserted as plain strings in
scope-filter.test.ts, so they run on the ubuntu CI too. I developed onWindows, and
npx vitest runthere has a set of pre-existing failuresunrelated to this change (a
gstack:tddskill name that is illegal in a Windowspath,
scope.test.tshome-dir assertions, and several git-worktree tests). Iconfirmed those same failures on a clean checkout of the base commit before
starting.
Docs
docs/designs/team-intelligence-platform.mdrecords theusage.jsonllineformat, so it now shows the
cwdfield and what the two filters do with anevent that lacks one.