fix(report): each scope reports its own dashboard sessions once, against its own snapshots (#785, #786) - #791
Conversation
…in it (Tencent#785) Every scope read one machine-wide events.jsonl and picked its sessions out by cwd prefix. The user scope excluded nothing, so a user-scope pull reported every project's sessions (and, through the shared reported snapshots, took them from the project's own report); Copilot sends no cwd, so a project never reported its Copilot sessions; and a raw cwd under a symlink or /tmp never matched the realpath'd projectRoot. The hook now stamps each event's dataHome with the data home of the scope the dispatcher resolved (the key the per-scope usage file already uses), and a report keeps only its own scope's events, comparing realpath'd keys. A project also owns its in-repo .teamai key, where hooks record until migration moves it to a partition. Events written before this carry no dataHome: a project keeps those whose realpath'd cwd is under its root, the user scope never reports them. The log stays machine-wide for the dashboard UI, stats --by-repo, session save and the contribute check. Removes the excludeProjectRoots option, which pull only ever passed as [] (the user target exists only when no project config resolved), and the projectRoot option now carried by selfConfig. The usage guide documents how to remove by hand a skill an earlier release pushed into stats/<user>.yaml from another project.
15c0e78 to
20bf78e
Compare
|
Findings
The PR description includes both a test plan and real-CLI/e2e evidence, so no description-related finding is needed. No earlier findings were provided to mark as resolved. |
- Events record `dataHomeKey`, a hash of the realpath'd data home, instead of the path. A Copilot event persisted a workspace path through its data home (the raw root for a non-git project, the path-derived partition name otherwise), breaking the path-free Copilot contract from Tencent#666. - A data home that no longer exists (an in-repo .teamai removed after migration) keys through its parent's realpath, so it still matches the key recorded while it existed. - A non-git project's root is realpath'd before older events' cwd is matched against it, as the cwd already was. - A key that is not a string (a hand-edited log) counts as absent instead of throwing and skipping the whole report. - The legacy `dashboard-report` command's stamping is asserted. - CHANGELOG and the comment say teamai does not record Copilot's cwd, not that Copilot sends none.
) The manual `stats/<user>.yaml` cleanup sat under single-repo mode, but the pre-Tencent#748 leak hit every team with a git-kind repo, so it moves to "Usage reporting" and notes where an `http` team repo keeps the file. The guide also says the scope key is per event: hooks that run outside the project (a worktree removed before the session ends) report to the scope they ran in.
The CHANGELOG now says a session in a directory that resolves to no project (a non-git project's subdirectory, a submodule or nested clone) is the user scope's, as for skill usage. The usage guide drops the line on http team repos: pull does not report usage to them, so no stats file there needs cleaning.
…encent#786) The report sends per-session deltas against reported-*.json snapshots that every scope shared. A session whose events belong to two scopes (a cd into another project mid-session) was then reported by the first scope, and the second compared its own part with the first scope's totals and sent nothing. Each scope now keeps its snapshots in <dataHome>/dashboard/, and the user scope, whose data home holds the shared files, in user-reported-*.json. The first time a scope needs one it copies the shared file, so the first report after the upgrade sends nothing already reported; after that it reads only its own. The user scope moves too, unlike the ticket proposed: had it kept writing the shared file, a project seeding later would copy the user scope's part of a split session and report nothing for its own. The shared file is no longer written, except by an earlier release after a rollback, which only a scope not yet seeded reads.
|
Findings
Resolved Earlier Finding
The PR description contains a test plan and real-CLI/e2e evidence, so no description-related blocking finding is needed. |
…arted in (Tencent#785, Tencent#786) A Stop carries the whole transcript's totals (prompts, tokens, interventions, request cost). Filtered per event, a session that moved into another scope mid-session was reported whole again by the scope holding the later Stop: 3 user-scope prompts then 2 in P reported 3 to the user team and 5 to P. Each session is now decided once, by its first keyed event, and reported whole by that scope. This replaces Tencent#786's "a split session reaches both teams with its part"; per-scope snapshots stay, so a session ID another scope already reported (Copilot's PID fallback) still counts as new. Unkeyed sessions from before the upgrade are decided by their first cwd. The user scope now takes those whose directory still exists and resolves to it (resolveConfigForDir, the dispatcher's rule) instead of dropping its whole backlog; no cwd, or one removed since, is still no scope's. The Copilot test also runs a payload without cwd from a hook in the project.
|
Findings
Resolved Earlier Finding
The PR description includes sufficient test-plan and real-CLI/e2e evidence. |
…nts-scope # Conflicts: # CHANGELOG.md # docs/designs/data-directory-layout.md # docs/usage-guide.md # docs/usage-guide.zh-CN.md # src/__tests__/dashboard-report-scope.test.ts # src/__tests__/e2e/report-timeout.test.ts # src/__tests__/git-kind-reports.test.ts # src/__tests__/scope-filter.test.ts # src/__tests__/self-mode-no-business-reset.test.ts # src/__tests__/team-push-interventions.test.ts # src/dashboard-collector.ts # src/dashboard.ts # src/hook-handlers.ts # src/team-push.ts # src/types.ts
…ent#785, Tencent#786) `teamai stats` (Tencent#771) still called filterEventsByScope with the old { projectRoot, excludeProjectRoots } options, synchronously, after Tencent#795 made it async and keyed by the scope config, so main no longer type-checks and stats-scope fails. It also subtracted the shared reported-*.json, which no scope writes since Tencent#786. stats now filters with the config it resolved and subtracts that scope's own snapshots (readReportedInterventions / readReportedPromptTokens, the report's readers), so what it shows matches what pull reports. The user scope leaves a project's older sessions out, as the report does (Tencent#785); the stats-scope case that pinned "no exclusion in the user scope" now expects that.
…nts-scope # Conflicts: # src/__tests__/stats-scope.test.ts # src/stats.ts
…nts-scope # Conflicts: # docs/designs/data-directory-layout.md
|
Findings
Resolved Earlier Findings
The PR description includes sufficient real-CLI/e2e evidence. |
A session ID now names one run up to its session_end or process_exit. A PID-fallback ID (Copilot) comes back for a later run, maybe in another scope, and the log keeps the ended run below the compaction threshold, so grouping by ID alone gave the later run to the first run's scope. Each run is still decided whole by its first keyed event. Events written by main since Tencent#795 record the data home as a path (`dataHome`); the report now keys them the way the writer derives `dataHomeKey`, so pending Copilot sessions (no cwd) are not dropped.
|
Findings
Resolved Earlier Findings
|
A later run of a reused session ID (Copilot's PID fallback) was decided on its own but returned under the same ID, so aggregation and the per-scope snapshots merged two runs in one scope back into one session. The filter now returns a later run as `<id>@<first event timestamp>`; the first run keeps the bare ID, so existing snapshots still match. An unkeyed event's cwd under a project root counted even when the directory was gone (realpath fell back to the raw path). It now counts only while it exists, as the docs and the user-scope rule already say.
|
Findings
Resolved Earlier Findings
|
|
Findings
Resolved Earlier Findings
|
Run identity no longer depends on which earlier runs compaction kept: every run is `<id>@<first event timestamp>`, so a reused PID-fallback ID is a new session even when the scope's snapshot still names the run compaction dropped. Snapshot entries keyed by the bare ID (written by earlier builds) are adopted by the first run of that ID in the log, so the upgrade re-sends nothing; the next snapshot holds only run IDs. An unkeyed event's cwd is now owned by the scope resolveConfigForDir resolves it to, for projects as for the user scope, so a nested clone under a project is no longer reported by both. The lexical root matcher and its string-level tests go; the cases move to real repositories.
…hout tokens (Tencent#785) A Codex session is now kept per rollout whenever its Stops name a rollout, not only once a Stop carries a token record, so a tokenless resumed rollout is not compared against the dropped one's totals. Each rollout also keeps its corrections (a correction goes to the rollout of its prompt), its active time (each gap to the rollout of the event it ends at) and its request costs, and a dropped rollout adds them to the intervention and daily sums, with its cache tokens from its tokens. The prompt-token snapshot, which holds the rollouts, is written with any delta, so a rollout whose rejections alone moved keeps its new totals.
|
Addressing review 5823872303, all four findings, in
Entries written before rollouts were kept also consume and carry these fields: the leftover intervention entry, the daily active time and the request costs go into the prior rollout. All four tests are red on |
|
Findings
Review Notes
|
…llout's failure (Tencent#785) The daily snapshot took the request costs of the latest rollout only, so with rollout A still in the log a rollout B was compared against A's costs and clamped; a Codex session's daily costs now sum its rollouts. Each rollout also records whether it failed (an error, an interruption or a correction). A dropped rollout that failed keeps the session unsuccessful, and one with a correction keeps it corrected, so a clean later rollout does not turn it into a success.
|
Addressing review 5824055200, both findings, in
Both tests are red on |
|
Findings
Review Notes
|
…pts, per rollout (Tencent#785) A Codex session whose tokens come from the thread-level counter (tokenScope session) was not split into rollouts, so its prompts, interventions, active time, costs and failure were compared against a dropped rollout's. It is now kept per rollout like the others; the counter already spans the rollouts, so no rollout holds tokens of its own and the session total stays that counter's. A Codex Stop may count no prompts, so a rollout's prompts are its Stop's count or else its own submits: a dropped rollout's submit-counted prompts are no longer lost.
|
Addressing review 5824192700 in P1 (modern Codex rollouts with session-scoped tokens): fixed. The The same area had one more gap, fixed here as well: a Codex Stop may count no prompts, because the transcript scan counts Claude-format turns. A rollout's prompts are therefore its Stop's count, or else its own submits, so the submit-counted prompts of a dropped rollout are no longer lost. Tests:
Both tests are red on |
|
Findings
Review Notes
|
…s writes no seed (Tencent#785) A whole entry an earlier release left became a prior rollout carrying its tokens, which were then added to a thread-level counter that already holds them: rollout B's counter at 530 after A's 500 re-sent 500. A session whose counter spans its rollouts now takes no tokens from a dropped or prior rollout. `teamai stats` only reads, but seeding a scope's first snapshot wrote it with the current time, which a later report reads as the time an entry from before covers, taking a rollout begun earlier as reported. A read that does not persist now writes no seed, and a written seed keeps the shared file's time.
|
Addressing review 5824316403, both findings, in
One harness note: the daily check in the session-scoped Codex test now sums request costs across days. A request's cost goes to its own day, which can differ from the session's near midnight UTC. Both tests are red on |
|
Findings
Resolved Earlier Findings
The PR description includes sufficient test-plan and representative real-CLI/e2e evidence. |
…y's costs (Tencent#785) parseDailySnapshot() dropped the top-level pricedRequests, costMicros, cache tokens and priceVersion a daily entry from before per-day costs held, so an entry from before rollouts were kept lost its cost in the prior rollout, and a later rollout's cost was compared against it and omitted. They are now read as the session day's request costs, as computeDailyStatsDelta already reads them.
|
Addressing review 5824480263 in P1 (legacy daily cost fields): fixed. Test: the earlier release's daily entry holds $100 as session fields and its rollout A is compacted; rollout B, begun later, costs $20 on the same day. Reported: $20. Before this commit it was $0. The test is red on
|
|
Findings
Resolved Earlier Finding
The PR description includes sufficient representative real-CLI/e2e evidence. |
… request cost (Tencent#785) Rollout tracking recognized only `codex`, not `codex-internal` or `tcodex`, which write the same rollouts; it now uses isCodexTool(). A rollout's cost was read from requestDaily only, so an older Stop's requestMetrics left the rollout without cost, and the daily snapshot, which sums rollouts, omitted it; it is now that Stop's day's cost, as outside rollouts.
|
Addressing review 5824654269, both findings, in
Both tests are red on |
|
Findings
Resolved Earlier Findings
The PR description includes sufficient test-plan and representative real-CLI/e2e evidence. |
|
Addressing review 5824760190 in P1 (per-rollout Stop snapshots in timestamp order): fixed. A rollout's prompts, interventions and request costs now keep the latest Stop by its timestamp, the same rule For the record: the report and
|
|
Findings
Review Notes
|
…as far as it had got (Tencent#785) Migrating a whole entry from before rollouts were kept consumed it with each covered rollout's current totals, so a rollout begun before the entry was written but grown since had its later prompts taken as reported: an entry of 6 (A's 5, B's 1) with B now at 3 reported nothing. It now consumes it with each rollout's totals as of the entry's write, the metrics of the events up to then; what a rollout has done since is new.
|
Addressing review 5824911159 in P1 (an active rollout wholly covered by a legacy snapshot): fixed. An entry from before rollouts were kept is now consumed with each covered rollout's totals as of the entry's write. Those totals come from the metrics of the session's events up to that time ( Test: the entry holds 6 prompts, A's 5 plus B's first, and A is compacted. B had 1 prompt when the entry was written and has 3 now. Reported: 2. Before this commit it was 0. The test is red on
|
|
Findings
Review Notes
|
…try's cutoff before its push (Tencent#785) Both credit paths applied only when the parts' prompts exceeded the owner's, so a part that reported more active time, tokens or costs with no more prompts was sent again by the owner. The owner's entry is now raised counter by counter to at least the credit. An earlier release wrote its snapshot after the push, so events that arrived during the push predate the snapshot's time without being in it. The team stats file in the scope's reports checkout was written after that report read the log and before the push; the earlier of the two times is now the cutoff an entry from before covers.
|
Addressing review 5825021252, both findings, in P1 (split-session credit gated on prompts): fixed. Both credit paths, the one from the parts' events still in the log and the one from the owners file once they are gone, now raise the owner's entries to at least the credit, counter by counter. That covers prompts, each token field, interruptions, rejections, corrections, daily prompts, active time, cache tokens, and each day's request costs. Flags and dates stay the owner's own. So a part that reported more time, tokens or costs with no more prompts is no longer sent again. One note on the tokens example: with cumulative Stops, the latest part dominates on prompts and tokens both, and it is the owner the migration picks. The additive counters are where the gate bit. Test: one prompt; P's part was active 4 minutes, Q's (after a pause, no new prompt) 3 more, and each scope reported its own part. Neither scope sends time again. Before this commit Q re-sent P's 4 minutes. P1 (snapshot mtime as the coverage watermark): tightened. Nothing can be persisted for a release that has already run. But that release wrote the team stats file in the scope's own reports checkout after it read the log and before its push, and wrote the snapshot only after the push. The cutoff an entry from before covers is now the earlier of the two times. That leaves the report's own computation, milliseconds, instead of the push, and it falls back to the snapshot's time where there is no such file (HTTP). The entries this build writes keep per-rollout totals and do not depend on it. Test: the stats file is written 25 minutes ago and the snapshot 15 minutes ago; B had 1 prompt when the log was read, reached 2 during the push, and has 3 now. Reported: 2. Before this commit it was 1. Both tests are red on |
|
Findings
Review Notes
|
Fixes #785
Fixes #786
Part of #752
Summary
main already carries a first version of both tickets (the #795 squash,
352cfc4); this PR finishes them. Head:2482931(origin/main5e5b86dmerged: #813, #817).2482931(review 5825021252): a split session's credit raises the owner's entries counter by counter, not only on prompts; an entry from before covers up to the earlier of its snapshot's and its team stats file's times (the stats file was written before that report's push).640c547(review 5824911159): an entry from before covers a rollout still running only as far as it had got when written.c8b97ee(review 5824760190): a Codex rollout keeps its latest Stop by timestamp for prompts, interventions and costs, as for tokens.aade8ea(review 5824654269): every Codex variant (codex-internal,tcodex) is kept per rollout, and a rollout keeps an older Stop'srequestMetricscost.f073b8f(review 5824480263): a daily entry from before per-day costs keeps its session cost fields as its day's costs when parsed.53d1dab(review 5824316403): a Codex session whose token counter spans its rollouts takes no tokens from a dropped or prior rollout;teamai statswrites no seed, and a written seed keeps the shared file's time.467be7e(review 5824192700): a Codex session is kept per rollout also when its tokens come from the thread-level counter (no rollout then holds tokens of its own), and a rollout's prompts are its Stop's count or else its submits.12bd7bc(review 5824055200): a Codex session's daily request costs sum its rollouts; a dropped rollout that failed or was corrected keeps the session unsuccessful or corrected.0acbfe5(review 5823872303): a Codex session is kept per rollout with or without a token record; each rollout also keeps its corrections, active time and request costs, which a dropped rollout adds to the intervention and daily sums; the prompt-token snapshot holding the rollouts is written with any delta.de2552a(review 5823553860): an entry from before rollouts were kept covers the rollouts begun by its file's last write (read before this report writes it); the dropped rollouts' remainder is kept as one prior rollout. Dropped rollouts' totals, now with their Stop interruptions and rejections, reach the intervention and daily sums too.Maintainer decisions: the deviations from Each scope reports only the dashboard sessions recorded in it #785/Dashboard report snapshots per scope #786 below are accepted (recorded on both issues), and the migration logic stays in this PR.
9d0b130(reviews 5822175105, 5823044300): a Codex session is reported per rollout; a rollout compaction dropped keeps its reported totals under a hash of its path, so a resumed rollout with restarted counters is reported in full.09b8728(5823044300): a compacted split session's credit places a part with no Stop by the session's transcript (prompts in order, each with its directory), so a part a cumulative Stop already counts is not added again.79f2288: Stop-derived interventions take the greatest part, corrections add.abe6f40, no behavior change: scope attribution moves tosrc/dashboard-scope.ts, the owners index tosrc/session-owners.ts; the report andteamai statssharereportedBaselines().37b5319: real-CLI e2e of a compacted session from a workspace-data project resumed in another project.6930aba(from a review of the full diff against Each scope reports only the dashboard sessions recorded in it #785/Dashboard report snapshots per scope #786): a scope's first snapshot copies a tool's own session entries whole, as main did, so a session reported before fix(report): each scope keeps its own reported dashboard snapshots (#786) #795 and compacted before that scope's first pull is not sent again when resumed; only fallback entries are limited to the scope's runs. A legacy entry withouttokensno longer throws while its bare ID is split across runs. Review 5822175105 (Codex rollout totals after compaction) is answered in the thread: the clamp already applies on main in the same scope, and only Codex builds that write a new rollout per resume hit it; follow-up.CI review 5820895836 P1 ×3: a scope's snapshot entry is evidence of ownership only when the shared snapshots (all three, interventions included) hold none of it or the scope is past their total, so a copy, even the only one, names no owner. A session main split across scopes whose events are gone gets a credit from the parts' snapshots, carried on the owner's line and applied once: Stop-ended parts (daily entry) count their greatest cumulative total once, parts with no Stop add their prompts.
CI review 5820319860 P1 (per-scope baselines): a session main split across scopes per event is credited with the union of every part it reported (each scope's shortest event prefix reaching its snapshot), so parts counted before a cumulative Stop are not sent again and cumulative Stops are not credited twice. P1 (Copilot): traced to where it started by Copilot's own session log, found by the ID (
session.startcontext; no path stored, feat: add privacy-safe Copilot telemetry #666). P3: compaction keeps a session whose tool process is still running.CI review 5820008176 P1 (compacted in-workspace project): a tool's own session with no owner goes to the scope its transcript started in (Claude's first
cwd, which a resume from another project keeps; a Codex rollout'ssession_meta), when that scope's snapshots already hold it. Hooks recordtranscriptPathon UserPromptSubmit and SessionEnd as well as Stop (not SessionStart; never Copilot). The other two points were answered in the PR thread: P3 already handled (test sincefd1f163); the "largest subtotal" P1 holds only for Copilot, since transcript-backed Stops carry cumulative totals.CI review 5819849768: P3 (legacy exit before the next run's first prompt) was already handled, since the run split is recomputed from the whole log on every report; a test now pins it. The in-workspace P1 is left for a maintainer's judgment (see Merge Danger).
CI review 5819482771 P1 (tied copies): a greatest total tied across scopes names no owner (the copy main made of a shared entry in every scope); a report claims an ID of its own snapshots only when they show it reported it (absent from the shared snapshot, or past its total). P3: a crashed fallback run superseded by another
monitorPidcounts as the run closed before, so its late unannotated exit does not close the new run. The in-workspace P1 is the migration's boundary; see Merge Danger.CI review 5819098160 P1 ×4 (owner migration): a tool's own ID in any of a scope's three snapshots is the scope's holding its greatest total (prompts, then tokens), so a split session, a scope past its shared seed, and a session with only an intervention count all keep the right owner. Besides the user scope and partitions, it reads in-workspace data homes that a
cwdstill in the log leads to, and each report records the unowned IDs of its own snapshots.CI review 5818783645 P1 (owners from existing snapshots):
session-owners.jsonlis first written from the per-scope snapshots the base version left, before any scope can claim an ID; created exclusively. (Refined by review 5819098160, above.)CI review 5818536370 P1 (per-run status in migration): a run taking its share of a summed daily snapshot keeps its own
succeededandcorrected, so an adopted run changes no status total.CI review 5818536370 P3 (late unannotated exit): replaces the 15 s window. An unannotated
process_exitfollowed by more events of its fallback ID before the next start, with an earlier run of the ID closed, did not end the open run; it joins the run closed before, however late.CI review 5818293263 P1 (shared baseline across scopes): seeding from the shared snapshot splits the whole log into runs, lets every scope's runs of a bare ID consume its entry in log order, and keeps the shares of the scope's own runs. Interventions and daily follow the shared prompt-token coverage in the same order.
CI review 5818293263 P1 (ownership after compaction): the scope that first reports a tool's own session ID appends
{sessionId, dataHomeKey}to~/.teamai/dashboard/session-owners.jsonl(no path; first line wins). A recorded session is its owner's wherever it is resumed, whatever the log still holds.CI review 5818293263 P1 (pre-upgrade dashboard exit): an unannotated
process_exitthat arrived after the next run began belongs to the run closed before it (a 15 s window here; review 5818536370 replaced it, see above).CI review 5817909397 P1 (identity after compaction): a tool's own session ID is keyed by the ID itself, as on main, not
<id>@<first timestamp>. A session resumed after compaction dropped its events still reads what its scope reported. Only fallbackpid-…runs carry the timestamp.CI review 5817909397 P1 (unreported later run): a bare entry is the sum of the runs of its ID in the log at the earlier release's last report (compaction keeps or drops an ID's runs together). Those runs now consume it in log order, each taking up to its own totals of what is left. Once the prompt-token entry is used up, the later runs count as new sessions; interventions and daily follow the prompt-token coverage. The first run always takes a share.
CI review 5817755607 P1 (resume): an end splits runs only on a fallback ID (
pid-…). A tool's own session ID is one session whatever ends it records:claude --resumecontinues it, in a new process, and its Stop carries the whole transcript, so a second run would report it again, maybe in another scope.CI review 5817221518 P1 (aggregate bare entry): an earlier release summed every run of a reused ID under the bare entry. Now each of those runs in the log but the last is taken as reported at its own totals, and the last takes the entry, so no run is sent again. This applies to the scope snapshot at report time,
teamai stats, and the seed from the shared file.CI review 5817221518 P1 (crash, no end): a
session_starton a fallback ID (pid-…) whosemonitorPiddiffers from its open run's begins a new run. A tool's own ID is not split this way: Claude fires SessionStart again on resume, in a new process, and its Stop carries the whole transcript.CI review 5816682111 P1 (path-keyed adoption): a scope's first snapshot is seeded only with the shared entries of its own runs in the log, under their run IDs, and none for a run recorded with a
dataHomepath (that release already kept per-scope snapshots). An unmatched shared entry is dropped, so a later reuse of the ID cannot inherit it.CI review 5816682111 P1 (delayed
process_exit): the dashboard monitor recordsprocessExitAfter, the last event it observed, and the filter closes only that run. An exit appended after the next run of the same ID began no longer ends it; an exit whose run compaction dropped is ignored.CI review 5815574898 P1 (
adoptBareKeys): a bare snapshot entry goes only to a run an earlier release recorded (first event withoutdataHomeKey), so a PID reuse this build records is never taken for another scope's reported run.CI review 5815574898 P1 (duplicate end): a second
session_end/process_exitwith nothing recorded since the first joins the run it closed, instead of counting as one more session.This replaces #786's "a split session reaches both teams with its part". A Stop carries the whole transcript's totals (prompts, tokens, interventions, cost). So per event, a session with 3 user-scope prompts that moves into P for 2 more reported 3 to the user team and 5 to P. Now it is reported once, 5, by the scope it started in. Per-scope snapshots stay for session IDs that recur in another scope (Copilot's PID fallback).
Evidence
Maintainer round:
npx vitest runandnpm run test:e2eon09b8728.09b87286930aba)09b8728)09b8728)de2552a)de2552a)de2552a)de2552a)0acbfe5)0acbfe5)12bd7bc)12bd7bc)467be7e)467be7e)53d1dab)tcodexA 5 prompts, compacted; B 2f073b8f)requestMetricscosting 40f073b8f)aade8ea)c8b97ee)640c547)640c547)79f2288)37b5319)fd1f163)CI review 5820895836:
npx vitest runon442f4c2vsa56be8e. No e2e, per the review instructions.a56be8e442f4c2CI review 5820319860:
npx vitest runona56be8evs10e9d92. No e2e, per the review instructions.10e9d92a56be8esplitto P, then 2 to Q, no Stop; events still in the logcopilot-wreported by W (<W>/.teamai), W's events compacted, continued in Q; Copilot's session log starts in WmonitorPidalive, at compactionCI review 5820008176 P1:
npx vitest runon10e9d92vsfd1f163. Claude's resume behavior verified with Claude Code 2.1.281 in an isolatedCLAUDE_CONFIG_DIR, API unreachable:claude --resume <id>from another project appends to the original transcript, whose firstcwdis still the original project; SessionStart'stranscript_pathon that resume names a file that never exists. No other e2e, per the review instructions.fd1f16310e9d92<W>/.teamai), no W event left in the log; resumed in Q with a transcript that started in Wsession_meta.payload.cwd= W)CI review 5819849768 P3: run 1 ends, run 2 starts, a pre-upgrade dashboard appends an unannotated exit; a pull (2 sessions); then run 2's prompt and Stop, another pull: still 2 sessions and 2 prompts, not a third run. The test passes on
ae37f00with no code change.CI review 5819482771:
npx vitest runonae37f00vs9e8fef6. No e2e, per the review instructions.9e8fef6ae37f00copied: 1(main's copies); compaction; resumed in P with a Stop of 2pid-1crashed (no end), new start from anothermonitorPid, unannotated exit, promptCI review 5819098160:
npx vitest runon9e8fef6vscd44499(the merge, before the fix). No e2e beyond the #809 file, per the review instructions.cd444999e8fef6movedas 3 prompts to P, then 5 to Q; compaction; resumed in P with a Stop of 6resumed: 1, P reported through 2; resumed in Q with 3quietwith no prompts (intervention snapshot only); resumed in Q<W>/.teamai) reportedresumed, another W session in the log; resumed in QMerge with main (#813):
repo-attribution-worktrees-809.test.tsexpected the repo to count 5 sessions, includingcd-1, which starts in HOME. With this PR a session is reported whole by the scope it started in (#785), so the expectation is 4; the file passes, 6 of 6.CI review 5818783645:
npx vitest runonc086326vs6be15dd. No e2e, per the review instructions.6be15ddc086326resumedin P (P's own snapshot, no owners file); compaction;claude --resumein QCI review 5818536370:
npx vitest runon6be15ddvsed84421. No e2e, per the review instructions.ed844216be15ddpid-Nruns, one successful and one interrupted (aggregate unsuccessful); daily delta after adoptionpid-1ended, next start, unannotatedprocess_exit1 min later, then a promptCI review 5818293263:
npx vitest runoned84421vsae037b3. No e2e for this round, per the review instructions.ae037b3ed84421pid-7as 1 prompt, then recorded P's run of that ID unreported; first reportsclaude --resumein Qpid-1ended, next start, an unannotatedprocess_exit1 s later, a promptsession-owners.jsonlafter a P report of Copilotcopilot-pCI review 5817909397:
npx vitest runonae037b3vs18dcd77.18dcd77ae037b3claude --resume(samesession_id) in P; P report<id>: 1), run 2 came after its last report; P's first report, from the shared snapshot and from P's ownpid-1: 6, runs of 5 and 3pid-1: 5, runs of 5 and 3CI review 5817755607:
npx vitest runon18dcd77vs93d4c0c. Per the request, no new real-CLI e2e for this one; the existingreport-scope-reuse.test.tsanddashboard-scoping.test.tspass on the18dcd77build.93d4c0c18dcd77claude --resume(samesession_id) in P; reportssession_end/process_exit, then a start in PCI review 5817221518, on
93d4c0cvse74c166(the same branch before this fix, with origin/maina725574merged).e74c16693d4c0c<id>: 2prompts); P's first report, from the shared snapshot or P's own (main since #795)pid-Ncrashed with no dashboard running (no end); the next invocation reusespid-Nin PReal CLI (
report-scope-reuse.test.ts, second test, builtdist/index.js, git provider), threeteamai pulls in P: two legacy runs of one fallback ID summed in the shared snapshot; then a run that crashed with no end; then the next invocation of that ID from anothermonitorPid.e74c166prompts / sessions93d4c0cprompts / sessionsCI review 5816682111, on
f37dd22vsbde8604.bde8604f37dd22pid-Nrun reported into the shared snapshot by an earlier release; samepid-Nin P recorded with main'sdataHomepath; P's first report (log retained, compacted, or P seeded before the reuse)process_exitfor the first; one more prompt and StopReal CLI (
report-scope-reuse.test.ts, builtdist/index.js, git provider): the first row throughteamai pull. Onbde8604the pull pushes no report at all (teamai-reportsnever created); onf37dd22it reports 1 prompt, 1 session. Theprocess_exitis the one the realteamai dashboardmonitor appended for a deadmonitorPid(processExitAfterequals the first run's last timestamp); the race is replayed by placing SessionEnd and the next SessionStart before it. Then pull reports 2 prompts, 2 sessions, a second pull pushes nothing, andteamai statsshows 2 sessions, 2 turns. The monitor race itself is replayed, not timed.CI review 5815574898, on
bde8604vs78763ff: real CLI, Copilot on git, no session ID (pid-<shell>).78763ffbde8604pid-Nrun in the user scope recorded and reported by v0.26.0-beta.2 (shared bare-ID snapshot; the ended run stays in the log); samepid-Nin P recorded by the new build; P's first pullprocess_exitappended as the dashboard monitor writes it; P pullThe first row needs a release before #795: main since #795 writes the user scope's own snapshot, so P seeds nothing from it (with origin/main as the recorder both builds give P 1). The
process_exitline is appended the waydashboard.tswrites it; the monitor loop itself was not run.CI review 5815196738, on
78763ffvsd28f86f: real CLI, Copilot on git, no session ID, P pulls only.d28f86f78763ffpid-Nrun in P recorded and reported by mainec56a67(bare-ID snapshot); P pull (nothing new)pid-Npid-N@<ts>(all 3)pid-Nin P; P pullCI review 5814734341, on
d28f86fvsdd3bc9f: real CLI, git, P pulls (and a user pull for the nested leg).dd3bc9fd28f86fpid-Nrun in P, ended, P pull; log emptied (compaction); samepid-Nin P, P pulldd3bc9f(bare-ID snapshot); P pull withd28f86f; +1 prompt; P pull<P>/nested(nested clone) and<P>/src; user pull, P pull<P>/removed-worktree)CI review 5814458097, on
b583dba(and13985fbbefore the merge of main) vsdd3bc9f: real CLI, Copilot on git, no session ID, P pulls only.dd3bc9fb583dbapid-Nruns in P, each ended, P pull after each, log keeps bothteamai statsin P: 2 sessions<P>/srcand<P>/removed-worktree(gone), P pullCI review 5814180921, on
dd3bc9fvs1e8e25f: real CLI, Copilot on git, no session ID (sopid-<shell>), nothing compacted.1e8e25fdd3bc9fpid-Nrun in user scope, ended, user pull; samepid-Nin P, P pulldataHomepath, no cwd), P pullEarlier matrix, real CLI on
1e8e25fvs origin/main57afe76(baseline build); sandbox HOME with a user scope and a git project P, each with its own bare remote. Agent × provider: Claude, Codex, CodeBuddy, OpenCode × git, gitlab, github, every cell identical. Counts fromteamai-reports:stats/<member>.yaml.Attribution: in P, an agent session under P, one under a symlink to P, and a Copilot session. User pull first, then P. The run fails if any report ran before the pulls.
Snapshots:
pid-Nreported by U, then the same ID in PThe moved-session row is the same on main only because main's user scope takes everything. OpenCode sends no session ID (it is derived from the cwd), so it cannot move scope; it runs the upgrade and pending rows.
teamai stats(main'sfd0e913fix, same filter) on a sandbox after the pulls: user scope "No usage data yet", P 3 sessions, 3 turns;57afe76showed 3 and 6.Tests:
dashboard-report-scope.test.tsdrives the real dispatcher and the real report. It covers:pid-Nreused in P after the user scope reported it (fails on shared snapshots), and again while the log still holds the ended run (fails on per-ID attribution);pid-Nruns in P, reported between and after: 2 sessions, 2 prompts (fails when runs share the ID); and after compaction dropped the reported run (fails when the first run keeps the bare ID);adoptBareKeys);pid-Nan earlier release reported for the user scope, reused in P by this build, before and after P's first report: 1 session, 1 prompt (fails when any first run adopts); a run in progress across the upgrade still adopts (prompts 1, sessions 0);process_exit: 1 session (fails when every end closes a run);process_exitafter the next run of the same ID started: 2 sessions, 2 prompts (fails when an exit closes the open run);session-owners.jsonlholds no path;dataHome-path run in P under apid-Nthe user scope reported into the shared snapshot, with the log retained, compacted, or P seeded before the reuse: 1 session, 1 prompt (fails when the seed copies the shared file or adopts for path-keyed runs);dataHomeKey(before Each scope reports only the dashboard sessions recorded in it #785) or with thedataHomepath (main since fix(report): each scope keeps its own reported dashboard snapshots (#786) #795);P-abgo to the user scope; a removed dir under P and no cwd go to no one.scope-filter.test.tspins the key and session rules. It covers:session_endorprocess_exit, a second end joining the run it closed; adataHomepath keyed likedataHomeKey;adoptBareKeys(first run only, never over a run's own entry, bare entry retired, none for a run whose first event hasdataHomeKey,dataHome-path and in-progress runs still adopt);session_end/process_exitand a resume;monitorPidstays addressable: its late unannotated exit does not split the new run;process_exitfollowed by more of the open fallback run joins the run closed before, 1 s or 1 min after the start; one followed by a new start closes the open run;monitorPidopens a new run, forpid-Nandpid-N-<cwd>; the same process, or a tool's own ID, stays one run;session-trends.test.ts: a run adopting its share of a summed daily snapshot keeps its own status, so the delta is 0;adoptBareKeys: runs consume the bare entry in order; a run the sum does not reach takes nothing; the first always takes a share; a run's own entry kept; a tool's own ID untouched;dataHome-path run, a scope entry still is;.teamaikey, also after migration removed it;stats-scope.test.ts: an older session from an unconfigured directory now counts in the user scope (1), P's stays out.Test plan
On
2482931(origin/main5e5b86dmerged; conflicts in the design doc,dashboard.ts,dashboard-collector.ts,hook-handlers.tsand two tests resolved by keeping both sides:dataHomeKeyplus #813'sprojectAnchor):npx tsc --noEmitnpx vitest run: 4863 passed, 1 skipped (4841 on both sides of theabe6f40move)npm run test:e2e: 256 passed, 26 skipped, includingrepo-attribution-worktrees-809.test.tsand the new real-CLI resume inreport-scope-reuse.test.tsnpm run build, plus the CI-review 5817221518 real-CLI run above (fails one74c166at the first pull) and the 5816682111 one (fails onbde8604at the first pull)1e8e25fMerge Danger
Door: two-way.
dataHomeKeyis optional; older builds ignore it. Main'sdataHomeevents (only between352cfc4and this merge) are keyed from that path. The new~/.teamai/dashboard/session-owners.jsonland the snapshots' per-rolloutsegmentsare ignored by older builds, but once shipped later builds must keep reading them.Blast Radius: reporting and
teamai stats(same filter)pid-…ID ends atsession_endorprocess_exit.session_endorprocess_exit, not atsession_start: Claude fires SessionStart again on compact and resume, and a Stop after it still carries the whole transcript. A run that crashed with neither end recorded keeps its ID open; a later run on a fallback ID starts anew only when itssession_startrecords anothermonitorPid. A tool's own ID reused after a crash, or a recycled PID resolving to the samemonitorPid, still joins the open run. A reused ID whose first recorded event is an end (its earlier events lost) joins the run that ended before it.~/.teamai/dashboard/session-owners.jsonl(the ID and a data home key, no path). The file is append-only and grows by one line per such session, like the snapshots; concurrent reports from two scopes append independently, and the first line for an ID wins. The file is first written from the snapshots an earlier release left: each ID goes to the scope whose snapshots hold its greatest total (prompts, then tokens); a tie names no owner, since it is main's copy of a shared entry, and the scope a resume runs in reports only its delta against its own copy. Scopes read: the user scope, every partition, and in-workspace data homes acwdstill in the log leads to; each report also records the unowned IDs of its own snapshots. A session none of these reach (a workspace-data project, non-git or not migrated, with no event left in the log) is found by its transcript: Claude's firstcwdsurvives a resume from another project, and a Codex rollout keeps itssession_meta; the scope that origin resolves to owns it when its snapshots hold the ID. Copilot is traced the same way through its own session log, found by the session ID, so no path is stored (feat: add privacy-safe Copilot telemetry #666). This relies on the tools' own formats (documented as internal for Claude): if one changes, the lookup finds nothing and the previous rule applies. OpenCode (no explicit ID) and CodeBuddy (origin field unverified) keep the previous rule. Not verified: how Copilot writes its session log on a cross-directory resume.session_start'smonitorPid), so a recycled PID can keep a finished session in the log a little longer.realpath/resolveConfigForDironly; the Windows string normalization for them is gone. Windows handling of these events is unverified (no Windows CI or machine). Keyed events compare hashes and are unaffected.process_exitfrom a monitor before this build has noprocessExitAfter. Followed by more events of its fallback ID before the next start, with an earlier run closed, it joins that earlier run; otherwise it keeps the previous rule (ends the open run, which then records nothing more to split).pid-…) entries only for its runs still in the log, so a reused PID cannot inherit one.teamai statswrites none); a report on this build that writes the file and then fails to push moves that time forward, so a rollout begun before it is taken as covered.process_exit) or a deleted cwd are reported by no one.