Skip to content

fix(report): each scope reports its own dashboard sessions once, against its own snapshots (#785, #786) - #791

Merged
jeff-r2026 merged 48 commits into
Tencent:mainfrom
SaulMoro:fix/785-dashboard-events-scope
Sep 25, 2026
Merged

jeff-r2026 merged 48 commits into
Tencent:mainfrom
SaulMoro:fix/785-dashboard-events-scope

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

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/main 5e5b86d merged: #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's requestMetrics cost.

  • 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 stats writes 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 to src/dashboard-scope.ts, the owners index to src/session-owners.ts; the report and teamai stats share reportedBaselines().

  • 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 without tokens no 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.start context; 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's session_meta), when that scope's snapshots already hold it. Hooks record transcriptPath on 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 since fd1f163); 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 monitorPid counts 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 cwd still 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.jsonl is 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 succeeded and corrected, so an adopted run changes no status total.

  • CI review 5818536370 P3 (late unannotated exit): replaces the 15 s window. An unannotated process_exit followed 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_exit that 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 fallback pid-… 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 --resume continues 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_start on a fallback ID (pid-…) whose monitorPid differs 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 dataHome path (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 records processExitAfter, 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 without dataHomeKey), 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_exit with nothing recorded since the first joins the run it closed, instead of counting as one more session.

 hook-dispatch → ~/.teamai/dashboard/events.jsonl          # also legacy `dashboard-report`, dashboard PID monitor
-  { dataHome: "/Users/…/.teamai/projects/Users-…-proj-…" }    # a path: a Copilot line leaked the project (#666)
+  { dataHomeKey: sha256(realpath(dataHome))[:16] }            # no path

 filterEventsByScope(events, scope S)
-  per event: dataHome == S          unkeyed → project if cwd under root, user never
+  per SESSION, decided by its first keyed event, reported whole
+    a tool's own ID: one session, whatever ends it records (resume continues it)
+    a fallback pid-… ID: one run up to its session_end / process_exit
+  a second end with nothing recorded since the first (monitor's process_exit after SessionEnd) joins the run it closed
+  a process_exit with processExitAfter joins the run holding that event, closes it only if still open; that run gone → dropped
+  an unannotated process_exit followed by more of its pid-… ID before the next start (earlier run closed) joins the run closed before it
+  a tool's own ID recorded in session-owners.jsonl → its owner's scope, whatever the log still holds
+    the file starts from every known scope's three snapshots: each ID to the scope with its greatest total
+    (user scope, partitions, and in-workspace data homes a cwd in the log leads to)
+    evidence only where the shared snapshots (all three) lack the ID or the scope is past them; a tie or a lone copy names no owner
+    several scopes' evidence → the owner's line carries a credit: greatest Stop-ended part + parts with no Stop; applied once
+    each report also records its own snapshots' unowned IDs by the same evidence rule
+  a tool's own ID with no owner → the scope its transcript's origin cwd resolves to, if that scope's snapshots hold it
+    Claude: first cwd; Codex: session_meta; Copilot: its own session log (found by ID), session.start context
+  a run whose events name ≥ 2 main-era data homes → the owner credits the union of each scope's reported prefix

 compactEvents
+  keeps a session whose latest session_start's monitorPid is still running
+  a session_start on a pid-… ID from another monitorPid than its open run's begins a new run (a crash left it open)
+  a tool's own ID is returned as `<id>`, as on main: compaction does not change it
+  a fallback run is returned as `<id>@<its first timestamp>`: aggregation and snapshots count runs apart, and compaction does not change the ID
+  key = dataHomeKey, or dataHomeKey(dataHome) for main's path-keyed events
+  unkeyed session → the scope resolveConfigForDir(cwd) resolves to (cwd must exist; a nested clone under P is not P's)
+                    no cwd / cwd gone → no one

 adoptBareKeys(snapshot, scope's events)       # report and `teamai stats`
+  a snapshot entry keyed by a bare fallback `<id>` (earlier builds) sums its runs: they consume it in log order, each up to its own totals
+    prompt tokens decide coverage: once used up, later runs are new sessions; interventions and daily follow; the first run always takes a share
+  only runs whose first event has no dataHomeKey (an earlier release recorded them; main's `dataHome` path counts)
+  seeding from the shared file: every scope's runs consume it in log order, the scope keeps its own runs' shares; a `dataHome`-path run takes nothing
+  the report writes that snapshot at once, even with nothing new to report, so the file holds only run IDs

 snapshots <dataHome>/dashboard/reported-*.json, ~/.teamai/dashboard/user-reported-*.json   (per scope, seeded once from the shared file)

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 run and npm run test:e2e on 09b8728.

scenario before 09b8728
Codex rollout A (5 prompts, 500 input tokens) reported and compacted; resumed as rollout B (2 prompts, 30 tokens) +0 (6930aba) +2 prompts, B's tokens
the same, A reported by an earlier release as a whole entry and compacted before this build's first report +0 (09b8728) +2 prompts, B's tokens
Codex rollout A (5 prompts, 1 rejection) reported and compacted; rollout B (2 prompts, 1 rejection) daily prompt turns 5 (09b8728) 7; rejections 2
Codex rollouts with no token record: A 5 prompts, compacted; B 2 5 (de2552a) 7
Codex correction in A, compacted; one in B 1 (de2552a) 2
Codex A 8 active minutes, 100 cache-read tokens, compacted; B 1 minute, 20 B's alone (de2552a) 9 minutes, 120
Codex A's rejections 1 → 2 with no prompt/token change, compacted; B 1 2 (de2552a) 3
Codex A $100 reported; B $20 while A is still in the log $100 (0acbfe5) $120
Codex A interrupted, reported, compacted; B clean sessionsSucceeded 1 (0acbfe5) 0
modern Codex (session-scoped counter) A 5 prompts, $100, counter 500, compacted; B 2 prompts, $20, counter 530 5 prompts (12bd7bc) 7 prompts, $120, 530 tokens
Codex A 3 submits, Stop with no prompt count, compacted; B 1 submit 3 (12bd7bc) 4
entry from before: 5 prompts, counter 500; A compacted; B's counter 530 +530 tokens (467be7e) +30
shared snapshot 2 h old; rollout B after it; baselines read without persisting, then a report seed created now, B covered (467be7e) no seed; B's 2 prompts sent
entry from before with $100 as session cost fields; A compacted; B $20 $0 (53d1dab) $20
tokenless tcodex A 5 prompts, compacted; B 2 5 (f073b8f) 7
Codex Stop with requestMetrics costing 40 0 (f073b8f) 40
(unit) rollout's newer Stop $100 appended before an older $40 $40 (aade8ea) $100
entry from before of 6 prompts (A 5, B 1); A compacted; B now 3 0 (c8b97ee) 2
split session, one prompt; P active 4 min, Q 3 more, each reported Q re-sends P's 4 min (640c547) nothing re-sent
entry from before: stats file 25 min ago, snapshot 15 min ago; B 1 → 2 during the push → 3 1 (640c547) 2
main reported 3 prompts in P before any Stop, then Q's cumulative Stop of 5; compacted; Stop of 6 +0 (79f2288) +1
the reverse: Q's Stop of 2, then P's 3 with no Stop; Stop of 6 +1 +1
P's Stop 1 interruption, Q's cumulative Stop 2, a third part with no Stop; Stop with 3 +0 interruptions (37b5319) +1
real CLI: non-git W reports a Claude session; owners index gone, W's events compacted; resumed in Q (appending to W's transcript) with a Q session of its own; both pull Q reports both (fd1f163) Q its own only; W the resumed turn

CI review 5820895836: npx vitest run on 442f4c2 vs a56be8e. No e2e, per the review instructions.

scenario a56be8e 442f4c2
P1: only P pulled on main, so only P holds a copy of a user-scope session; resumed in the user scope P reports it P nothing; user +1
P1: a zero-prompt session only in the shared and P's intervention snapshots; P reports another session, then it resumes in the user scope P claims it and its stats change P unchanged; user +1
P1: main reported 3 prompts in P and 2 in Q, no Stop; events compacted; resumed with a Stop of 6 +3 +1 in total

CI review 5820319860: npx vitest run on a56be8e vs 10e9d92. No e2e, per the review instructions.

scenario 10e9d92 a56be8e
P1: main reported 3 prompts of split to P, then 2 to Q, no Stop; events still in the log P sends +2 again neither sends anything
control: each part ended in a cumulative Stop (P 3, Q 5), then a prompt with a Stop of 6 — 1 prompt in total
P1: Copilot copilot-w reported by W (<W>/.teamai), W's events compacted, continued in Q; Copilot's session log starts in W Q reports it again Q nothing
P3 (unit): a session a stale exit marked stopped, its monitorPid alive, at compaction dropped kept (a dead-PID one is dropped)

CI review 5820008176 P1: npx vitest run on 10e9d92 vs fd1f163. Claude's resume behavior verified with Claude Code 2.1.281 in an isolated CLAUDE_CONFIG_DIR, API unreachable: claude --resume <id> from another project appends to the original transcript, whose first cwd is still the original project; SessionStart's transcript_path on that resume names a file that never exists. No other e2e, per the review instructions.

scenario fd1f163 10e9d92
Claude session reported by W (non-git, data home <W>/.teamai), no W event left in the log; resumed in Q with a transcript that started in W Q reports it again Q nothing
the same for a Codex rollout (session_meta.payload.cwd = W) Q reports it again Q nothing
control: transcript from W under an ID W never reported (a fork) Q reports it Q reports it

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 ae37f00 with no code change.

CI review 5819482771: npx vitest run on ae37f00 vs 9e8fef6. No e2e, per the review instructions.

scenario 9e8fef6 ae37f00
P1: shared, user and P snapshots each hold copied: 1 (main's copies); compaction; resumed in P with a Stop of 2 the user scope owns it and reports +1 user nothing; P +1
P3 (unit): pid-1 crashed (no end), new start from another monitorPid, unannotated exit, prompt three runs two; the exit joins the crashed run

CI review 5819098160: npx vitest run on 9e8fef6 vs cd44499 (the merge, before the fix). No e2e beyond the #809 file, per the review instructions.

scenario cd44499 9e8fef6
P1: main reported moved as 3 prompts to P, then 5 to Q; compaction; resumed in P with a Stop of 6 P reports it again P nothing; Q +1
P1: shared held resumed: 1, P reported through 2; resumed in Q with 3 Q re-sends Q nothing; P +1
P1: P reported quiet with no prompts (intervention snapshot only); resumed in Q Q +1 intervention session Q 0
P1: non-git project W (<W>/.teamai) reported resumed, another W session in the log; resumed in Q Q re-sends Q nothing

Merge with main (#813): repo-attribution-worktrees-809.test.ts expected the repo to count 5 sessions, including cd-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 run on c086326 vs 6be15dd. No e2e, per the review instructions.

scenario 6be15dd c086326
P1: main reported Claude resumed in P (P's own snapshot, no owners file); compaction; claude --resume in Q Q reports it again and records itself as owner Q nothing; P nothing (its snapshot already holds it)

CI review 5818536370: npx vitest run on 6be15dd vs ed84421. No e2e, per the review instructions.

scenario ed84421 6be15dd
P1: two reported pid-N runs, one successful and one interrupted (aggregate unsuccessful); daily delta after adoption +1 sessionsSucceeded, 0 sessionsEnded all 0
P3: pid-1 ended, next start, unannotated process_exit 1 min later, then a prompt the prompt opens a third run two runs; the exit joins the first
control: the same exit followed by a new start closes the open run closes the open run

CI review 5818293263: npx vitest run on ed84421 vs ae037b3. No e2e for this round, per the review instructions.

scenario ae037b3 ed84421
P1: a release before #666 reported the user-scope Copilot run of pid-7 as 1 prompt, then recorded P's run of that ID unreported; first reports P 0 (its run spent the user run's baseline) P 1 session, 1 prompt; user nothing
P1: Claude session reported by P, log emptied (compaction), claude --resume in Q Q reports it again Q nothing; P 1 session, 1 prompt
P1 (unit): pid-1 ended, next start, an unannotated process_exit 1 s later, a prompt three runs (the exit closed the new run) two; the exit joins the first
session-owners.jsonl after a P report of Copilot copilot-p no file the ID only; neither P's root nor its data home

CI review 5817909397: npx vitest run on ae037b3 vs 18dcd77.

scenario 18dcd77 ae037b3
P1: Claude session reported by P, log emptied (compaction), claude --resume (same session_id) in P; P report P 2 sessions, 2 prompts P 1 session, 1 prompt
P1: an earlier release reported run 1 of a fallback ID (<id>: 1), run 2 came after its last report; P's first report, from the shared snapshot and from P's own P 0 (run 2 suppressed) P 1 session, 1 prompt, 1 intervention session
unit: bare pid-1: 6, runs of 5 and 3 5 and 6 5 and 1
unit: bare pid-1: 5, runs of 5 and 3 5 and 5 5, second run new

CI review 5817755607: npx vitest run on 18dcd77 vs 93d4c0c. Per the request, no new real-CLI e2e for this one; the existing report-scope-reuse.test.ts and dashboard-scoping.test.ts pass on the 18dcd77 build.

scenario 93d4c0c 18dcd77
P1: Claude session in the user scope, SessionEnd, user report; claude --resume (same session_id) in P; reports P 1 session (the transcript again) P 0; user 1 session, 2 prompts
unit: a tool's own ID after session_end / process_exit, then a start in P 2 runs, P takes the second 1 run, the user scope's

CI review 5817221518, on 93d4c0c vs e74c166 (the same branch before this fix, with origin/main a725574 merged).

scenario e74c166 93d4c0c
P1: an earlier release recorded two runs of one fallback ID in P and reported them as one (<id>: 2 prompts); P's first report, from the shared snapshot or P's own (main since #795) re-sends the second run: 1 prompt, 1 session nothing sent
P1: a user-scope Copilot run under pid-N crashed with no dashboard running (no end); the next invocation reuses pid-N in P P 0 sessions (all to the user scope) P 1 session, 1 prompt; user 1 session, 1 prompt

Real CLI (report-scope-reuse.test.ts, second test, built dist/index.js, git provider), three teamai 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 another monitorPid.

after pull e74c166 prompts / sessions 93d4c0c prompts / sessions
1 (legacy runs only) 1 / 1 (second legacy run re-sent) no report pushed
2 (+ crashed run) 2 / 2 1 / 1
3 (+ next invocation) 3 / 2 (merged into the crashed run) 2 / 2

CI review 5816682111, on f37dd22 vs bde8604.

scenario bde8604 f37dd22
P1: user-scope pid-N run reported into the shared snapshot by an earlier release; same pid-N in P recorded with main's dataHome path; P's first report (log retained, compacted, or P seeded before the reuse) P 0 sessions, 0 prompts P 1 session, 1 prompt
P1: Copilot run in P, SessionEnd, next invocation with the same ID starts, then the monitor's delayed process_exit for the first; one more prompt and Stop P 3 sessions P 2 sessions, 2 prompts

Real CLI (report-scope-reuse.test.ts, built dist/index.js, git provider): the first row through teamai pull. On bde8604 the pull pushes no report at all (teamai-reports never created); on f37dd22 it reports 1 prompt, 1 session. The process_exit is the one the real teamai dashboard monitor appended for a dead monitorPid (processExitAfter equals 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, and teamai stats shows 2 sessions, 2 turns. The monitor race itself is replayed, not timed.

CI review 5815574898, on bde8604 vs 78763ff: real CLI, Copilot on git, no session ID (pid-<shell>).

scenario 78763ff bde8604
P1: pid-N run in the user scope recorded and reported by v0.26.0-beta.2 (shared bare-ID snapshot; the ended run stays in the log); same pid-N in P recorded by the new build; P's first pull P 0 sessions, 0 prompts P 1 session, 1 prompt
P1: Copilot run in P ended by SessionEnd, then process_exit appended as the dashboard monitor writes it; P pull P 2 intervention sessions P 1 intervention session

The 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_exit line is appended the way dashboard.ts writes it; the monitor loop itself was not run.

CI review 5815196738, on 78763ff vs d28f86f: real CLI, Copilot on git, no session ID, P pulls only.

scenario d28f86f 78763ff
P1: pid-N run in P recorded and reported by main ec56a67 (bare-ID snapshot); P pull (nothing new) snapshots keep pid-N snapshots hold only pid-N@<ts> (all 3)
then log emptied (compaction); same pid-N in P; P pull P 1 session, 1 prompt (reuse hidden) P 2 sessions, 2 prompts

CI review 5814734341, on d28f86f vs dd3bc9f: real CLI, git, P pulls (and a user pull for the nested leg).

scenario dd3bc9f d28f86f
P1: Copilot pid-N run in P, ended, P pull; log emptied (compaction); same pid-N in P, P pull P 1 session, 1 prompt P 2 sessions, 2 prompts
P1: upgrade. Claude session in P reported by dd3bc9f (bare-ID snapshot); P pull with d28f86f; +1 prompt; P pull — nothing re-sent (1 session, 1 prompt), then 1 session, 2 prompts
P1: pre-#785 sessions in <P>/nested (nested clone) and <P>/src; user pull, P pull user 1, P 2 (nested counted twice) user 1, P 1
earlier rows (two runs kept in the log; <P>/removed-worktree) P 1 session / P 2 P 2 sessions / P 1

CI review 5814458097, on b583dba (and 13985fb before the merge of main) vs dd3bc9f: real CLI, Copilot on git, no session ID, P pulls only.

scenario dd3bc9f b583dba
P1: two pid-N runs in P, each ended, P pull after each, log keeps both P 1 session, 2 prompts P 2 sessions, 2 prompts; teamai stats in P: 2 sessions
P2: pre-#785 sessions under <P>/src and <P>/removed-worktree (gone), P pull P 2 sessions P 1 session

CI review 5814180921, on dd3bc9f vs 1e8e25f: real CLI, Copilot on git, no session ID (so pid-<shell>), nothing compacted.

scenario 1e8e25f dd3bc9f
P1: pid-N run in user scope, ended, user pull; same pid-N in P, P pull U 1 session, 2 prompts (P's too); P 0 U 1 session, 1 prompt; P 1 session, 1 prompt
P2: Copilot session in P recorded by main (dataHome path, no cwd), P pull P 0 P 1 session, 1 prompt

Earlier matrix, real CLI on 1e8e25f vs origin/main 57afe76 (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 from teamai-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.

recorded / pulled by user team team P
main / main 2 sessions, 3 prompts (all of P's) 0
this PR / this PR 0 2 sessions, 3 prompts; 6/6 events keyed, no path in Copilot lines
main / this PR (old events) 0 2 sessions, 2 prompts (old Copilot event has no cwd: no one)
main, not reported / this PR (pending) 1 prompt (its own, from an unconfigured dir) 2 prompts

Snapshots:

scenario main this PR
session moves user → P (3 + 2 prompts), pulled U, P, U+P U 5, P none U 5, P none, stable over re-pulls
main reported, first pulls after upgrade — 0 new commits in either team
then +1 prompt in the old P session — P +1 exactly
Copilot pid-N reported by U, then the same ID in P P none P 1 session

The 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's fd0e913 fix, same filter) on a sandbox after the pulls: user scope "No usage data yet", P 3 sessions, 3 turns; 57afe76 showed 3 and 6.

Tests:

  • dashboard-report-scope.test.ts drives the real dispatcher and the real report. It covers:
    • a Copilot line holds no path; a Copilot payload without cwd, from a hook in P;
    • a moved session reported once, by where it started, both user → P and P → Q;
    • a pid-N reused 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);
    • two pid-N runs 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);
    • the first report after the upgrade sends nothing a bare-ID snapshot already reported (fails without adoptBareKeys);
    • an adopted bare entry is written back under the run ID only, with and without a delta, and a reuse after compaction is a new session (fails when the adoption is not persisted);
    • a pid-N an 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);
    • SessionEnd then the monitor's process_exit: 1 session (fails when every end closes a run);
    • a delayed monitor process_exit after the next run of the same ID started: 2 sessions, 2 prompts (fails when an exit closes the open run);
    • two runs an earlier release reported under one bare ID, from the shared snapshot and from P's own: nothing sent (fails when only the first run adopts);
    • a user-scope Copilot run with no end, its ID reused in P from another process: P 1 session, 1 prompt; user 1 and 1 (fails when a start joins the open run);
    • a Claude session ended in the user scope and resumed in P: P 0, user 1 session, 2 prompts (fails when an end splits a tool's own ID);
    • a Claude session resumed after compaction dropped its events: P still 1 session (fails when a tool's own ID is keyed by its first timestamp);
    • a later run of a bare ID the earlier release never reported, from the shared snapshot and from P's own: 1 session, 1 prompt (fails when the last run takes the whole entry);
    • a shared bare entry consumed in the order of every scope's runs: P 1 session, 1 prompt, user nothing (fails when the seed consumes one scope's runs alone);
    • a Claude session compacted in P and resumed in Q: Q nothing, P unchanged (fails without recorded owners); session-owners.jsonl holds no path;
    • the same when main reported it (only P's snapshot, no owners file): Q and P nothing (fails when owners start empty);
    • owner migration: a lone copy of a shared entry names no owner, nor does one only the shared intervention snapshot holds; a compacted split session is credited from its parts' snapshots (+1 for a Stop of 6 after 3 + 2); tied copies name no owner, so the scope it resumes in reports only its delta; the greater of two split totals owns the session; a scope past its shared seed owns it; an intervention-only session keeps its owner; a non-git project's session is found through the log (each fails on the merge before the fix);
    • a dataHome-path run in P under a pid-N the 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);
    • fixtures that model an earlier release now record without dataHomeKey (before Each scope reports only the dashboard sessions recorded in it #785) or with the dataHome path (main since fix(report): each scope keeps its own reported dashboard snapshots (#786) #795);
    • older events in real repos: P's subdir and a symlink to P go to P; an unconfigured dir, a nested clone under P and a sibling P-ab go to the user scope; a removed dir under P and no cwd go to no one.
  • scope-filter.test.ts pins the key and session rules. It covers:
    • a whole session following its first keyed event, and a keyed event winning over an earlier unkeyed one;
    • a reused ID decided anew after session_end or process_exit, a second end joining the run it closed; a dataHome path keyed like dataHomeKey;
    • a run's ID unchanged when compaction drops the earlier runs of its ID; adoptBareKeys (first run only, never over a run's own entry, bare entry retired, none for a run whose first event has dataHomeKey, dataHome-path and in-progress runs still adopt);
    • a tool's own ID stays one run across session_end / process_exit and a resume;
    • a crashed run superseded by another monitorPid stays addressable: its late unannotated exit does not split the new run;
    • a pre-upgrade exit reported before the next run's first prompt: that run keeps its ID once its activity follows, so a later pull adds no session;
    • a Claude or Codex session resumed elsewhere after compaction goes to the in-workspace project its transcript started in; a transcript whose ID that project never reported does not; the same for Copilot through its own session log;
    • a session main split across scopes: parts counted before any Stop are credited (nothing re-sent); parts ending in cumulative Stops are not over-credited (a new prompt counts once);
    • an unannotated process_exit followed 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;
    • a fallback-ID start from another monitorPid opens a new run, for pid-N and pid-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;
    • a delayed observed exit targets its own run across scopes and is dropped once that run is compacted; a shared bare entry is not adopted by a dataHome-path run, a scope entry still is;
    • Windows case and separators of data homes, and symlinked data homes;
    • the in-repo .teamai key, also after migration removed it;
    • a project rooted at HOME, and a non-string key.
    • The string-level cwd cases (Windows separators/case, UNC, POSIX backslash, trailing slash, prefix) are gone with the lexical matcher they tested; the POSIX ones are the real-repo cases above.
  • 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/main 5e5b86d merged; conflicts in the design doc, dashboard.ts, dashboard-collector.ts, hook-handlers.ts and two tests resolved by keeping both sides: dataHomeKey plus #813's projectAnchor):

  • npx tsc --noEmit
  • npx vitest run: 4863 passed, 1 skipped (4841 on both sides of the abe6f40 move)
  • npm run test:e2e: 256 passed, 26 skipped, including repo-attribution-worktrees-809.test.ts and the new real-CLI resume in report-scope-reuse.test.ts
  • npm run build, plus the CI-review 5817221518 real-CLI run above (fails on e74c166 at the first pull) and the 5816682111 one (fails on bde8604 at the first pull)
  • The 4 agents × 3 providers matrix ran on 1e8e25f

Merge Danger

Door: two-way. dataHomeKey is optional; older builds ignore it. Main's dataHome events (only between 352cfc4 and this merge) are keyed from that path. The new ~/.teamai/dashboard/session-owners.jsonl and the snapshots' per-rollout segments are ignored by older builds, but once shipped later builds must keep reading them.

Blast Radius: reporting and teamai stats (same filter)

  • A session is the scope's where it started. A session that starts outside a project and moves in is the user team's, whole.
  • A tool's own session ID is never split: a resumed session is the scope's where it started, and a tool that reuses its own ID for unrelated sessions would merge them (as on main). Only a fallback pid-… ID ends at session_end or process_exit.
  • A fallback run ends at session_end or process_exit, not at session_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 its session_start records another monitorPid. A tool's own ID reused after a crash, or a recycled PID resolving to the same monitorPid, 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.
  • Fallback snapshot keys become run IDs; a tool's own ID keeps its key. A bare fallback entry goes only to runs an earlier release recorded, so a reuse this build records is never hidden by one; those runs consume it in log order. That relies on the entry being the sum of the ID's runs in the log at that release's last report, which holds while compaction keeps or drops an ID's runs together. A rollback to main, which reads bare IDs, re-sends the fallback runs still in the log.
  • A tool's own session ID is its first reporting scope's, recorded in ~/.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 a cwd still 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 first cwd survives a resume from another project, and a Codex rollout keeps its session_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.
  • A session main split across scopes per event is credited once with the union of the parts it reported, rebuilt from its events still in the log (each scope's shortest prefix reaching its snapshot). If those events were compacted, the owner's line carries a credit from the parts' snapshots: the greatest Stop-ended part (daily entry) counts once and parts with no Stop add. Whether a part with no Stop came before another's cumulative Stop is read from the session's transcript (Claude: prompts in order with their directories); with no transcript, all parts add, which undercounts once but never re-sends.
  • Compaction keeps a session while its tool process is running (its latest session_start's monitorPid), so a recycled PID can keep a finished session in the log a little longer.
  • Remaining session-attribution edge cases, if Codex raises more, are left for the maintainer to judge: session runs are reconstructed from a log that carries no run ID; a run ID written by the hook would remove the class.
  • Unkeyed legacy events are matched by realpath/resolveConfigForDir only; 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.
  • 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.
  • A process_exit from a monitor before this build has no processExitAfter. 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).
  • A scope's first snapshot copies a tool's own session entries from the shared one whole, as main did (a resumed session is not sent again), and keeps fallback (pid-…) entries only for its runs still in the log, so a reused PID cannot inherit one.
  • A Codex session is reported per rollout (reported prompts, tokens, interruptions, rejections, corrections, active time, request costs and whether it failed, per rollout, under a hash of its path); the daily costs sum the rollouts, and dropped rollouts' totals and failure reach the prompt-token, intervention and daily snapshots. An entry written before this covers the rollouts begun by its file's last write, or the earlier write of the scope's team stats file (before that report's push), as far as each had got then; what a report on an earlier release computed in between is its only ambiguity (a seed keeps the shared file's time; teamai stats writes 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.
  • Unkeyed events with no cwd (older Copilot, a pre-upgrade dashboard server's process_exit) or a deleted cwd are reported by no one.
  • After a rollback and re-upgrade, a scope that already has its own snapshot re-sends what the older release reported meanwhile.
  • Stats already pushed are not rewritten.

…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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Namespace report snapshots by scope — src/team-push.ts:432: events are now filtered by dataHome, but all three acknowledgment snapshots remain machine-wide maps keyed only by sessionId. If an ID occurs in two scopes—especially Copilot’s path-free PID fallback, or a session whose resolved scope changes—the first report marks it consumed and the second scope reports zero or only a partial delta. Include the scope/data-home identity in snapshot keys or maintain separate snapshots per scope.
  • [P1 blocking] Preserve Copilot’s path-redaction guarantee — src/hook-handlers.ts:188: the unconditional assignment persists an absolute dataHome path for Copilot events, undoing the existing protection that removes cwd and rejects path-like fallback IDs. A project partition path can expose the user home and project-derived directory name. Store a non-path scope identifier or otherwise retain Copilot’s no-path invariant.
  • [P2 non-blocking] The Copilot regression test does not exercise its stated no-cwd case — src/__tests__/dashboard-report-scope.test.ts:126: it explicitly supplies cwd: root, which is what lets dispatch resolve the project. Omit cwd and set the hook process directory appropriately so the behavior claimed in the test name is actually covered.

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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Namespace report snapshots by scope — src/team-push.ts:432: filtering is scope-aware, but all three acknowledgment snapshots remain keyed only by sessionId. The first scope reporting a reused ID—especially Copilot’s PID fallback or a session split across scopes—can suppress or distort the other scope’s deltas. Key snapshots by dataHomeKey/scope or store separate snapshots per scope.
  • [P2 non-blocking] Preserve pending user-scope events during upgrades — src/team-push.ts:388: every unkeyed event is rejected by user-scope reports, permanently dropping legitimate user sessions recorded before upgrade, during rollback, or by an older dashboard process. Attribute cwd-bearing legacy events after excluding all known project roots, rather than discarding the entire user backlog.
  • [P2 non-blocking] The Copilot regression test still does not exercise its stated no-cwd case — src/__tests__/dashboard-report-scope.test.ts:125: the payload explicitly supplies cwd: root, allowing dispatch to resolve the project from the payload. Omit cwd and run the hook with the process directory set to the project.

Resolved Earlier Finding

  • Copilot path redaction is resolved: events now persist a hashed dataHomeKey rather than the absolute data-home path.

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.
@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Namespace acknowledgment snapshots by scope — src/team-push.ts:434: the three machine-wide snapshots remain keyed only by sessionId. If two scopes contain the same ID, the first report records its totals and the second treats them as already reported, suppressing or distorting interventions, prompts, tokens, and daily stats. Include the scope/data-home key in snapshot identity or keep per-scope snapshots.
  • [P2 non-blocking] Preserve legacy user-scope events during upgrades — src/team-push.ts:388: every unkeyed event is rejected for user scope. A user session recorded by an older CLI, rollback, or older dashboard process is therefore never reported unless its cwd happens to belong to a project scope.
  • [P2 non-blocking] Exercise the claimed Copilot no-cwd path — src/__tests__/dashboard-report-scope.test.ts:127: the test supplies cwd: root, so dispatcher scope resolution never exercises the process-directory fallback. Omit cwd and run the hook from the project directory.

Resolved Earlier Finding

  • Copilot path redaction is resolved: events store a hashed dataHomeKey, not the absolute data-home path.

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
@SaulMoro SaulMoro changed the title fix(report): each scope reports only the dashboard sessions recorded in it (#785) fix(report): each scope reports its own dashboard sessions once, against its own snapshots (#785, #786) Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Distinguish reused session IDs before assigning a whole session — src/team-push.ts:431: deciding groups the entire retained log solely by sessionId, so the first scope permanently owns every later event with that ID. For example, a completed Copilot pid-123 user session remains in a log below the 5,000-line compaction threshold; when Copilot later reuses pid-123 in project P, the user scope absorbs P’s new metrics and P reports nothing. The regression test hides this by manually emptying events.jsonl.
  • [P2 non-blocking] Read the legacy dataHome field during migration — src/team-push.ts:430: events written by the currently released implementation have dataHome, not dataHomeKey. Treating them purely as unkeyed permanently drops pending Copilot sessions because those events intentionally contain no cwd, despite dataHome identifying their scope exactly.

Resolved Earlier Findings

  • Snapshots are now stored per scope.
  • Copilot events persist a hash rather than an absolute path.
  • The Copilot no-cwd test now exercises the process-directory fallback.
  • Legacy cwd-bearing user events are now attributed through config resolution.

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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Preserve run identity through aggregation — src/team-push.ts:464: the filter distinguishes reused IDs into separate runs, but returns their original sessionId, after which aggregateSessionMetrics and aggregateDailySessions merge same-scope runs again. If Copilot reuses pid-N for two completed sessions in project P while both remain below the compaction threshold, the second run adds prompts but never increments P’s session count and its daily metrics are folded into the first run. Assign a run-specific aggregation key or aggregate each run separately.
  • [P2 non-blocking] Exclude deleted project directories as documented — src/team-push.ts:422: project attribution calls realPath(cwd), whose failure fallback returns the original path, so a legacy event under a deleted path such as <projectRoot>/removed-worktree still passes the lexical root check. Check existence before project ownership, matching the user-scope branch and the documented “deleted cwd belongs to no scope” behavior.

Resolved Earlier Findings

  • Snapshots are now per scope, Copilot stores a hashed key, the no-cwd fallback is exercised, legacy user events are resolved by configuration, reused IDs are split after end events, and legacy dataHome paths are migrated.
  • The PR description includes sufficient real-CLI/e2e evidence.

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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Keep reused-run IDs stable after compaction — src/team-push.ts:451: the first occurrence currently receives the bare sessionId, so identity depends on which older events remain in the log. If pid-N is reported in project P, ends, is removed by the 5,000-line compaction, and is later reused in P, the new run again receives pid-N; P’s retained snapshot then suppresses its session count and any metrics not exceeding the previous run. Use an intrinsic run-specific ID that survives removal of earlier runs.
  • [P1 blocking] Do not let parent projects claim legacy events from nested repositories — src/team-push.ts:415: project attribution uses only realpath containment, while user attribution uses resolveConfigForDir. For an unkeyed event in an existing nested clone/submodule under project P, P accepts it because its cwd is beneath P, while resolution falls back to the user scope—or resolves a nested project Q—so that scope accepts it too. The same legacy session is therefore reported to two teams, contrary to the PR’s report-once rule.

Resolved Earlier Findings

  • Per-scope snapshots, Copilot path redaction, no-cwd fallback coverage, legacy dataHome migration, deleted-cwd exclusion, and aggregation-time separation of retained reused-ID runs are resolved.
  • The PR description includes sufficient real-CLI/e2e evidence.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Keep reused-run IDs stable across compaction — src/team-push.ts:451: the first retained run always receives the bare sessionId. If an older pid-N run causes the current run to be reported as pid-N@timestamp, compaction can remove the older run while retaining the active current run; the next report renames it to pid-N and reports it again—or compares it against an unrelated earlier snapshot. Derive run identity from intrinsic event data rather than its position in the retained log.
  • [P1 blocking] Resolve project ownership instead of using containment — src/team-push.ts:415: an unkeyed legacy event inside an unconfigured nested clone/submodule is accepted by parent project P because its real path is below P, while the user-scope branch resolves that directory to the user config and accepts it too. The same session is consequently reported to two teams. Apply resolveConfigForDir consistently and require its resolved scope to match the project.

Resolved Earlier Findings

  • Per-scope snapshots, Copilot path redaction, no-cwd fallback coverage, legacy dataHome migration, deleted-cwd exclusion, and retained reused-run aggregation are resolved.
  • The PR description includes sufficient real-CLI/e2e evidence.

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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5823872303, all four findings, in 0acbfe5. A dropped Codex rollout now keeps every metric that restarts or that compaction removes.

  • P1 (tokenless rollouts): fixed. A Codex session is split into rollouts as soon as a Stop names one, not only once a Stop carries a token record. A resumed rollout that stopped before producing tokens is therefore not compared against the dropped rollout's totals. Test: A with 5 prompts and no tokens, reported and compacted, then B with 2: 7 prompts.
  • P1 (corrections): fixed. A correction is attributed to the rollout of its prompt, or else to the session's latest rollout. Each rollout keeps its count, and a dropped rollout adds it to the intervention sum. Test: one correction in A, compacted, then one in B: 2.
  • P1 (duration and cache): fixed. Each rollout keeps its active time (each gap goes to the rollout of the event that ends it) and its request costs per day. A dropped rollout adds both to the daily sums, and its cache-read and eligible tokens come from its own tokens. Test: A with 8 active minutes and 100 cache-read tokens, compacted, then B with 1 minute and 20: 9 minutes and 120.
  • P1 (segment updates without prompt/token changes): fixed. The prompt-token snapshot, which holds the rollouts, is now written with any delta. So a rollout whose rejections alone moved keeps its new totals. Test: A's rejections go 1 → 2 with prompts and tokens unchanged, A is compacted, then B has 1: 3.

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 de2552a. npx tsc --noEmit clean; npx vitest run: 4850 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 0acbfe5.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Sum request costs from retained Codex rollouts — src/team-push.ts:848: withDroppedRollouts() adds request metrics only for compacted rollouts, while aggregateDailySessions() keeps only the latest retained rollout’s requestDaily. If rollout A reports $100 and retained rollout B reports $20, B’s cost delta is clamped to zero and its $20 baseline replaces A’s, permanently losing B’s initial cost. Fold every current segment’s requestDaily into the daily snapshot.
  • [P1 blocking] Preserve failure state from compacted rollouts — src/team-push.ts:547: dropped rollout metrics are added to prompts, duration, cache, and interventions, but succeeded and corrected remain those of the current rollout. If rollout A was interrupted or errored, is compacted, and rollout B finishes cleanly, the session changes from failed to successful and increments sessionsSucceeded; later corrections can also increment sessionsCorrected again. Carry dropped failure/correction state into the effective daily snapshot.

Review Notes

  • The four findings from the immediately preceding review are resolved in the current diff.
  • The PR description includes sufficient test-plan and representative real-CLI/e2e evidence.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824055200, both findings, in 12bd7bc:

  • P1 (request costs of retained rollouts): fixed. A Codex session's daily request costs are now the sum of its rollouts still in the log, taken from each rollout's own Stop costs per day, instead of the latest rollout's alone. So rollout B's costs are no longer compared against A's and clamped. Test: A ($100) is reported, then B ($20) arrives while A is still in the log. Total cost: $120; it was $100 before this commit.
  • P1 (failure state of compacted rollouts): fixed. Each rollout records whether it failed (an error, an interruption, or a correction), and the snapshot keeps that. A dropped rollout that failed keeps the session unsuccessful in the effective daily snapshot, and one with a correction keeps it corrected. A clean later rollout therefore neither increments sessionsSucceeded nor, through a later correction, sessionsCorrected a second time. An entry written before rollouts were kept passes its daily outcome to the prior rollout. Test: A was interrupted, reported and compacted; B finishes cleanly: sessionsSucceeded stays 0.

Both tests are red on 0acbfe5. npx tsc --noEmit clean; npx vitest run: 4852 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 12bd7bc.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Segment modern Codex rollouts even when tokens are session-scoped — src/dashboard-collector.ts:1956: the !sessionSnapshot guard prevents m.segments from being created for current Codex transcripts containing token_usage_record. Consequently droppedRollouts() cannot preserve per-rollout prompts, interventions, duration, costs, or failure state. Concrete case: rollout A reports 5 prompts and $100, is compacted, then rollout B reports 2 prompts and $20 with tokenScope: 'session'; B’s non-token metrics are compared against A’s larger snapshots and produce zero delta. Keep per-rollout segments for non-token metrics while retaining the session-scoped token total.

Review Notes

  • The immediately preceding findings about summing retained rollout costs and preserving dropped rollout failure state are resolved for segmented rollouts.
  • The PR description includes sufficient representative real-CLI/e2e evidence.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824192700 in 467be7e:

P1 (modern Codex rollouts with session-scoped tokens): fixed. The !sessionSnapshot guard is gone, so a Codex session is kept per rollout even when its tokens come from the thread-level counter (tokenScope: 'session'). Its prompts, interventions, active time, request costs and failure state are all kept per rollout, like the others. The counter already spans the rollouts, so no rollout holds tokens of its own, and the session's token total stays the counter's; adding a dropped rollout's tokens would count them twice.

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:

  • A modern Codex rollout A (5 prompts, $100, counter at 500) is reported and compacted, then rollout B (2 prompts, $20, counter at 530). Result: 7 prompts, $120, and 530 input tokens, which is the counter's total, not 1030.
  • Rollout A has 3 submits and a Stop with no prompt count, and is compacted; then rollout B has 1 submit: 4 prompts.

Both tests are red on 12bd7bc. npx tsc --noEmit clean; npx vitest run: 4854 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 467be7e.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Do not add legacy tokens to a session-scoped counter — src/team-push.ts:513: after the base version reports Codex rollout A with 500 thread-level tokens and compaction removes it, resumed rollout B may expose the cumulative session counter as 530. Migration creates a 500-token prior rollout, then this line adds it to the already-cumulative 530, producing 1,030 and resending 500 tokens. Dropped token totals must only be added for transcript-scoped counters.
  • [P1 blocking] Preserve the source timestamp when teamai stats seeds snapshots — src/team-push.ts:198: teamai stats calls the baseline reader with persist=false, but this still writes a new scope snapshot with the current mtime. If rollout B began after the shared legacy snapshot but before that stats command, the next pull treats B as covered by the newly timestamped snapshot and suppresses its prompts/tokens. Preserve the shared snapshot’s coverage timestamp or avoid writing migration state from the read-only stats path.

Review Notes

  • The earlier session-scoped Codex segmentation finding is resolved: modern rollouts now retain per-rollout non-token metrics.
  • The PR description includes sufficient representative real-CLI/e2e testing evidence.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824316403, both findings, in 53d1dab:

  • P1 (legacy tokens added to a session-scoped counter): fixed. When a session's tokens come from one counter that spans its rollouts (tokenScope: 'session', now flagged on the session's metrics), neither a dropped rollout nor the prior rollout left by an entry from before carries tokens; only the transcript-scoped counters do. Test: the earlier release's entry holds 5 prompts and a counter at 500; rollout A is compacted, and rollout B shows the counter at 530. Reported: +30 input tokens. Before this commit it was +530.
  • P1 (teamai stats stamping the seed): fixed. A baseline read that does not persist, which is how teamai stats reads, now writes no seed and leaves it to the report. A seed the report writes keeps the shared file's modification time, so the time an entry from before covers stays the shared snapshot's, whoever seeds it. Test: the shared snapshot is written 2 h ago, rollout B begins after it, and the baselines are read without persisting. No scope snapshot is created, and the next report sends B's 2 prompts. Before this commit, the file was created with the current time and B counted as covered.

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 467be7e. npx tsc --noEmit clean; npx vitest run: 4856 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 53d1dab.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Preserve legacy daily cost fields when parsing snapshots — src/session-trends.ts:140: parseDailySnapshot() discards the supported top-level pricedRequests, costMicros, cache-token, and priceVersion fields instead of converting or retaining them. Concrete failure: rollout A has a legacy $100 daily snapshot and is compacted; rollout B adds $20. droppedRollouts() loses A’s cost, so B’s $20 is compared against the raw $100 baseline, produces zero delta, and is permanently omitted when the snapshot advances. Preserve these legacy fields or convert them into requestDaily.

Resolved Earlier Findings

  • Session-spanning Codex counters no longer add legacy rollout tokens.
  • teamai stats no longer writes a seed, and persisted seeds retain the shared snapshot timestamp.
  • The other previously reported scope, ownership, reuse, and compaction findings appear resolved.

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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824480263 in f073b8f:

P1 (legacy daily cost fields): fixed. parseDailySnapshot() now reads a daily entry's top-level pricedRequests, costMicros, cache tokens and priceVersion as the session day's request costs when the entry has no requestDaily. That is how computeDailyStatsDelta already reads them. As a result, an entry from before rollouts were kept carries its cost into the prior rollout, and a later rollout's cost is no longer compared against it and dropped.

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 53d1dab.

npx tsc --noEmit clean; npx vitest run: 4857 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on f073b8f.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Recognize every Codex variant when creating rollout segments — src/dashboard-collector.ts:1866: rollout tracking checks only event.tool === 'codex', although codex-internal and tcodex use the same transcript format. For a tokenless tcodex rollout A reported and compacted, rollout B’s restarted prompt/intervention counters are compared directly with A’s snapshot and can produce zero delta. Reuse isCodexTool() here.
  • [P1 blocking] Preserve legacy requestMetrics inside Codex rollout segments — src/dashboard-collector.ts:1892: segment costs are recorded only from event.requestDaily. An existing Codex Stop containing the supported legacy requestMetrics field therefore gets an empty segment; because aggregateDailySessions() prefers segments, its pending cost is omitted and can be permanently lost when the session is reported/compacted. Convert requestMetrics to the event date as the non-segment path does.

Resolved Earlier Finding

  • Legacy daily snapshot fields are now converted into requestDaily, resolving the previous cost-migration 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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824654269, both findings, in aade8ea:

  • P1 (every Codex variant): fixed. Rollout tracking now goes through isCodexTool(), so codex-internal and tcodex sessions are kept per rollout like codex. Test: a tokenless tcodex rollout A with 5 prompts is reported and compacted, then rollout B has 2. Reported: 7 prompts. Before this commit it was 5.
  • P1 (legacy requestMetrics in rollout segments): fixed. A rollout whose Stop records its cost as requestMetrics now keeps it as that Stop's day's cost, which is what the non-segment path does. The daily snapshot, which sums a Codex session's rollouts, therefore no longer omits it. Test: a Codex Stop with requestMetrics costing 40. Reported cost: 40. Before this commit it was 0.

Both tests are red on f073b8f. npx tsc --noEmit clean; npx vitest run: 4859 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on aade8ea.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Keep per-rollout Stop snapshots in timestamp order — src/dashboard-collector.ts:1883: record() unconditionally overwrites prompts, interventions, and request costs in append order, although overlapping background Stop handlers can append out of chronological order. If a newer Codex Stop containing $100 finishes first and an older $40 scan appends afterward, the rollout is persisted as $40 and the remaining $60 is never reported. Apply the same timestamp-aware latest-snapshot logic already used for tokens.

Resolved Earlier Findings

  • All previously reported findings appear resolved, including support for every Codex variant and migration of legacy requestMetrics.

The PR description includes sufficient test-plan and representative real-CLI/e2e evidence.

)

A rollout's prompts, interventions and request costs took the last Stop
appended, though background Stop handlers may append an older scan
after a newer one, which then replaced the newer totals. They now keep
the latest Stop by its timestamp, as the rollout's tokens already do.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824760190 in c8b97ee:

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 setLatestTokenSnapshot applies to its tokens. A background handler that appends an older scan after a newer one therefore no longer replaces the newer totals.

For the record: the report and teamai stats read the log through readEvents(), whose dedupeEvents() already returns events in timestamp order. So the reachable exposure was callers of aggregateSessionMetrics() that pass their own event arrays. The test drives that function directly: a newer Stop ($100, 3 prompts, 2 rejections) followed by an older one ($40, 2, 1). The rollout keeps $100, 3 and 2; before this commit it kept $40, 2 and 1. The test is red on aade8ea.

npx tsc --noEmit clean; npx vitest run: 4860 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on c8b97ee.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Do not treat an entire active rollout as covered by a legacy snapshot — src/team-push.ts:450: coverage is decided solely by segment.since <= writtenAt, then the rollout’s current totals are subtracted from the old aggregate. For example, the old snapshot contains rollout A’s 5 prompts plus B’s first prompt (6 total); A is compacted, then B grows to 3 prompts. Migration consumes all 3 current B prompts from the legacy baseline, reconstructs only 3 dropped prompts, and reports zero instead of B’s 2 new prompts. Preserve B’s totals as of the snapshot, rather than treating all later growth in a previously started rollout as reported.

Review Notes

  • The previously reported latest-Stop ordering issue is resolved.
  • The PR description includes sufficient representative real-CLI/e2e evidence.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5824911159 in 640c547:

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 (metricsAsOf), in both the report and teamai stats. They no longer come from the rollout's current totals, so whatever a rollout begun before the write has done since then counts as new.

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 c8b97ee.

npx tsc --noEmit clean; npx vitest run: 4861 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 640c547.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Apply split-session credit when non-prompt metrics increase — src/team-push.ts:650: both credit paths return early based only on prompt count (:650 and :679). If main reported a session with 1 prompt/100 tokens in P and later the same 1 prompt/150 cumulative tokens in Q, migration leaves P’s baseline at 100; a later 160-token Stop reports +60 instead of +10, duplicating Q’s 50 tokens. Interventions, duration, and costs can be duplicated similarly when prompts are unchanged.
  • [P1 blocking] Do not use snapshot mtime as the coverage watermark — src/team-push.ts:692: snapshots are written after the remote push, so events appended while that push is running predate the snapshot mtime but were never included in it. For example, an old report reads A=5 and B=1, B reaches 2 during the push, and later reaches 3; migration treats B=2 as covered and reports only one of the two new prompts. Persist the event-read cutoff or covered per-rollout totals instead.

Review Notes

  • The previous active-rollout migration finding is resolved for events actually present when the earlier report was calculated.
  • The PR description includes sufficient representative real-CLI/e2e evidence.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressing review 5825021252, both findings, in 2482931:

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 640c547. npx tsc --noEmit clean; npx vitest run: 4863 passed, 1 skipped; npm run test:e2e: 256 passed, 26 skipped on 2482931.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Use the dashboard-event read cutoff, not the stats-file mtime — src/team-push.ts:768: the stats file is also written after the report reads events.jsonl. If rollout B grows from 1 to 2 prompts while the old report is pulling/preparing the worktree, that report still writes stats and its snapshot from B=1, but both file mtimes follow B’s second event. Migration therefore treats B=2 as covered and reports only one when B later reaches 3, permanently losing a prompt. Persist the actual event-read cutoff or covered per-rollout totals.

Review Notes

  • The earlier counter-by-counter split-session credit finding is resolved.
  • The PR description includes sufficient representative real-CLI/e2e evidence.

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.

Dashboard report snapshots per scope Each scope reports only the dashboard sessions recorded in it

2 participants