Skip to content

fix(hooks): keep a removed worktree's hook events in its project (#810) - #824

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
SaulMoro:fix-810-deleted-cwd-scope
Sep 25, 2026
Merged

jeff-r2026 merged 1 commit into
Tencent:mainfrom
SaulMoro:fix-810-deleted-cwd-scope

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Once a session's worktree was removed, its later hook events and skill uses were recorded under the user scope, which counted the session, or were dropped when there was no user scope. They now keep the scope the session last recorded.

hook payload, cwd = removed worktree b
├── dispatcher (hook-dispatch)       -> dashboard, track, track-slash handlers,
│                                       contribute-check and pending-hint gates
└── legacy entry points              -> dashboard-report, track, track-slash, contribute-check
    all resolve through hookScopeDir(payload, tool)   src/dashboard-collector.ts
    (resolveHookConfig = resolveConfigForDir(hookScopeDir(...)))
 resolveHookConfig(payload, tool)
   cwd exists (or none)  -> resolveConfigForDir(cwd)          # unchanged, reads nothing else
-  cwd gone              -> resolveConfigForDir(cwd)          # the user scope, or null
+  cwd gone              -> last event of this session with a dataHomeKey, at this cwd first
+                           (events.jsonl, once per process)
+                           config = resolveConfigForDir(checkout of its projectAnchor)
+                             # the main checkout; for a bare repo, an existing worktree
+                           config unreadable              -> null (dropped, never the user scope, #748)
+                           dataHomeKey(getDataHome(config)) == dataHomeKey -> config
+                           otherwise / nothing recorded   -> resolveConfigForDir(cwd), as today
 eventProjectAnchor(cwd, sessionId)
+  cwd gone              -> the session's last projectAnchor, at this cwd first (as process_exit does)
After git worktree remove b Before After
Stop, SessionEnd, tool_use (user scope exists) user scope's dataHomeKey, no anchor project dataHomeKey, projectAnchor
same, no user scope dropped recorded under the project
skill uses (Skill tool, /skill) ~/.teamai/user-usage.jsonl project usage.jsonl
user-scope teamai stats Sessions: 1, team-skill 1 uses No usage data yet.
project config unreadable user scope dropped
share reminder (project turns it off, user scope on) shown from the user scope's settings not shown
a late Stop of b after the session moved to another repo user scope, other repo's anchor b's project and anchor

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run passes: 308 files, 4865 passed, 1 skipped
  • Added/updated tests: e2e deleted-worktree-scope-810.test.ts (12 cases); unit hook-config-810.test.ts (2)
  • E2E run locally: the new file plus report-scope-reuse, report-timeout, dashboard-scoping, dashboard-unified, repo-attribution-worktrees-809: 6 files, 43 passed. The full npm run test:e2e runs in CI.

The e2e test drives the built CLI: sandbox HOME, local team repo, a project in its partition, a worktree b outside the checkout, hook payloads on stdin.

in b:           prompt-submit, Skill use                         (dispatcher)
git worktree remove --force b
cwd = b:        prompt-submit /team-skill, tool_use, Skill use,  (dispatcher)
                SessionEnd, Stop
                dashboard-report, track, track-slash             (legacy entry points)
assert          every later event: project dataHomeKey + session's projectAnchor
                project usage.jsonl has 5 uses; user-usage.jsonl has none
                user-scope `stats` counts no session; project `stats` counts 1
variants        no user scope (recorded, not dropped); unreadable project config (dropped);
                bare repo proj/.bare + proj/main + proj/b; the session moved to another repo first;
                share reminder off in the project only: no reminder on Stop (dispatcher and legacy
                contribute-check), none from a stash
Build Result
origin/main 12 of 12 fail, e.g. expected [] to deeply equal [ 'prompt_submit', …(4) ] (no user scope: events dropped) and user stats shows Sessions: 1
this PR 12 of 12 pass

I removed each of 16 parts alone, rebuilt and re-ran the tests. Every removal made a test fail. The parts cover each entry point, the dispatcher's two track handlers, the three reminder gates, the anchor reuse, the same-cwd preference (scope and anchor), the unreadable-config rule, the data home check, the bare-repo checkout and the legacy tool-roots lookup.

Two existing tests changed, and neither assertion was loosened. The track handlers now take the dispatcher's scope, so hook-handlers.test.ts passes them the file's scope fixture instead of null, and git-kind-reports.test.ts resolves the scope through resolveHookConfig.

Reproduced with the real CLI (git mode, sandbox HOME)
$ teamai pull --force            # in repo: the legacy .teamai/ moves into the partition
$ git worktree add ../b -b b
  s-b in b: UserPromptSubmit, a Skill use          (hook-dispatch, payloads on stdin)
$ git worktree remove --force ../b
  s-b after the removal: a Skill use, Stop

# origin/main (5e5b86d9)
prompt_submit dataHome=~/.teamai/projects/…-repo-…  projectAnchor=<sandbox>/repo
stop          dataHome=~/.teamai                    projectAnchor=-
usage: 1 in the project, 1 in ~/.teamai/user-usage.jsonl
$ teamai stats    # user scope
  team-skill     1 uses   last: just now
  Sessions:           1

# this PR
prompt_submit dataHome=~/.teamai/projects/…-repo-…  projectAnchor=<sandbox>/repo
stop          dataHome=~/.teamai/projects/…-repo-…  projectAnchor=<sandbox>/repo
usage: 2 in the project, ~/.teamai/user-usage.jsonl absent
$ teamai stats    # user scope
No usage data yet.
$ teamai stats    # project
  team-skill     2 uses   last: just now
  Sessions:           1

From reading the code, not reproduced with the real CLI:

  • Legacy per-worktree .teamai/ installs fall back to today's answer (unit test), because their recorded dataHomeKey is not the main checkout's scope.
  • A payload without session_id gets the pid-based id its events carry (same host process).

Related Issues

Closes #810

Notes for Reviewers

  • Correction to the issue. usage-tracker.ts:31 means that the legacy ~/.teamai/usage.jsonl is never read. ~/.teamai/user-usage.jsonl is the user scope's own usage and is reported, so the skill uses reached the user-scope team.
  • Cost. A hook whose cwd exists does exactly what it did. Only a gone cwd reads events.jsonl, once per process.
  • Limits. Recovery needs the session's earlier events. Compaction (5,000 lines) keeps only active sessions, events from before [bug] stats, dashboard, session save and wiki slugs split one repo into one project per worktree #809 carry no anchor, and Copilot events record no directory, so they have no anchor either. In each case the answer is today's.
  • Reversible. No new fields and no migration.
  • Docs. data-directory-layout.md, usage-guide{,.zh-CN}.md and team-intelligence-platform.md.
  • Rebased on fix(report): each scope reports its own dashboard sessions once, against its own snapshots (#785, #786) #791. Events now record dataHomeKey (a hash of the realpath'd data home) in place of the dataHome path, so the recorded scope matches on dataHomeKey(getDataHome(config)). The e2e and unit tests assert and record the key. The real-CLI transcript above predates the rebase, when events recorded the path; the rebased build passes the same e2e (12 of 12).

@SaulMoro
SaulMoro force-pushed the fix-810-deleted-cwd-scope branch from 3dec2ed to 50a5bf9 Compare September 24, 2026 20:49
@jeff-r2026 jeff-r2026 self-assigned this Sep 24, 2026
@github-actions

Copy link
Copy Markdown

No findings.

The PR description provides sufficient testing evidence, including a representative real-CLI end-to-end run for the runtime behavior change. Per instruction, I reviewed the diff only and did not execute tests or PR code.

…cent#810)

A hook resolves its scope from the payload's cwd, and resolveConfigForDir
answers the user scope for a directory that no longer exists. So once a
session's worktree was removed, its remaining events (tool_use, SessionEnd,
Stop) and skill uses were recorded under the user scope, which then counted
the session and reported the skills to its team, or were dropped when there
was no user scope. The project lost the session's last snapshot.

resolveHookConfig (dashboard-collector.ts) is the one resolver for the
hook dispatcher and the legacy dashboard-report, track and track-slash
entry points. For an existing (or absent) cwd it is resolveConfigForDir, as
before, and reads nothing else. For a cwd that is gone it reads this
session's last event that recorded a dataHomeKey, once per process, preferring
the events recorded at that same cwd (a detached Stop can run after the
session moved on to another repo), and
resolves the config at that event's projectAnchor, the main checkout,
which still exists (for a bare repo, whose anchor is the git directory,
at one of its worktrees that still exists). It uses that config only when it is still the scope the
recorded dataHomeKey names, so a worktree's own legacy .teamai never becomes
the main checkout's scope. If that config exists but cannot be read, the
event is dropped rather than given to the user scope (Tencent#748). With nothing
to match (no events, events from before Tencent#809 without an anchor), it is
today's answer.

The dispatcher's track and track-slash handlers now use the dispatcher's
config instead of resolving their own, and eventProjectAnchor gives an
event whose cwd is gone the session's last anchor, as process_exit does.
The legacy track-slash looks skills up under the resolved scope's tool
roots before the cwd's. The share reminder's gates (contribute-check on
Stop, pending-hint on the next prompt) ask about hookScopeDir, the
directory resolveHookConfig resolves from, so a removed worktree's
session gets the project's reminder settings, not the user scope's. The
legacy `teamai contribute-check` command gates the same way. The hook
session id has one implementation, deriveDispatchSessionId in
utils/session-id.ts, shared by the dispatcher and the event writers.
@SaulMoro
SaulMoro force-pushed the fix-810-deleted-cwd-scope branch from 50a5bf9 to df5d0f8 Compare September 25, 2026 03:05
@github-actions

Copy link
Copy Markdown

No findings.

The PR description includes sufficient testing evidence, including a representative real-CLI end-to-end run for the runtime behavior change. The earlier review reported no findings, so there are none to mark resolved. I reviewed only the specified diff and did not execute PR code or tests.

@jeff-r2026
jeff-r2026 merged commit c7723d6 into Tencent:main Sep 25, 2026
11 checks passed
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.

[bug] hook events after a worktree is deleted fall back to the user scope

2 participants