Skip to content

fix(report): each scope keeps its own reported dashboard snapshots (#786) - #795

Merged
jeff-r2026 merged 2 commits into
Tencent:mainfrom
SaulMoro:fix/786-report-snapshots-per-scope
Sep 24, 2026
Merged

jeff-r2026 merged 2 commits into
Tencent:mainfrom
SaulMoro:fix/786-report-snapshots-per-scope

Conversation

@SaulMoro

Copy link
Copy Markdown
Collaborator

Fixes #786
Part of #752

Stacked on #791 (#785); review only the last commit.

Summary

 reportUsageToTeam(config)
   events   = filterEventsByScope(readEvents(), config)        # #785
-  reported = ~/.teamai/dashboard/reported-*.json              # one file for every scope
+  reported = scope snapshot                                   # #786
+    project: <dataHome>/dashboard/reported-*.json
+    user:    ~/.teamai/dashboard/user-reported-*.json
+    missing? copy ~/.teamai/dashboard/reported-*.json first   # file existing = seeded
   delta    = current - reported
   push stats/<user>.yaml                                     # format unchanged
-  write ~/.teamai/dashboard/reported-*.json
+  write scope snapshot
session X: 3 prompts in the user scope, then `cd` into P, 2 prompts

             shared snapshot                    per-scope snapshot
user pulls   U gets 3, shared[X] = 3            U gets 3, U[X] = 3
P pulls      P gets max(0, 2 - 3) = 0           P gets 2 - 0 = 2

Deviation from the ticket, agreed with the coordinator: the user scope moves to user-reported-*.json too, the same way #758 moved it to user-usage.jsonl. If it had kept writing the shared file, a project seeding after a user-scope pull would copy U[X] = 3 and still send P 0. The shared file is now only the seed. Only an earlier release writes it, after a rollback.

Evidence

Tests (src/__tests__/dashboard-report-scope.test.ts, which drives the real hook dispatcher and the real report):

test base (#785) this PR
split user scope / P: U gets 3, P gets 2 P gets 0 pass
split P / Q: P gets 3, Q gets 2 Q gets 0 pass
once seeded, the shared file is untouched and a rollback's rewrite of it is ignored shared file rewritten pass
first report after the upgrade sends nothing already reported pass pass
scope first seeded after a rollback skips what the old release reported pass pass

The last two tests pass on the base as well: they check the switch to per-scope files, not a bug the base has.

Real CLI (dist/index.js hook-dispatch + pull, Claude, a sandbox HOME, one bare remote per team). Each cell is that team's teamai-reports:stats/<user>.yaml:

split session           git / gitlab / github (identical)
  base  U: prompts=3            P: prompts=- sessions=-               <- P gets nothing
  new   U: prompts=3            P: prompts=2 sessions=1 turns=2

upgrade (before -> new), same on all three providers, before = origin/main and before = #785
  before: record + report       U: prompts=1 (2 commits)   P: prompts=2 (2 commits)
  new: first pulls              U: prompts=1 (2 commits)   P: prompts=2 (2 commits)   <- nothing pushed
  new: +1 prompt in old-p       U: prompts=1 (2 commits)   P: prompts=3 (3 commits)   <- +1 only

Test plan

  • npx tsc --noEmit, npx vitest run (4521 passed), npm run test:e2e, npm run build
  • Real CLI, split session and upgrade, providers git, gitlab, github, agent Claude. The change is in the report, which does not depend on the agent, so the other agents were not run.
  • In-repo data homes already git-ignore dashboard/ (init.ts, both .gitignore templates)

Merge Danger

Door: two-way

Rolling back leaves the per-scope files unused, and the older release goes back to the shared file. Re-upgrading after that: scopes that already have their own snapshot re-send whatever the older release reported while rolled back, as the ticket's "read only its own snapshot" implies.

Blast Radius: stats

Only the dashboard counts in stats/<user>.yaml (sessions, prompts, tokens, daily). Two known limits:

  • A session still running at the upgrade that later records in a second scope reports nothing there until that scope's part exceeds the seeded total.
  • A self-mode install whose migration was skipped (sync lock held) seeds in the repo's .teamai/dashboard/, which self migration does not move.

…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.
…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.
@jeff-r2026 jeff-r2026 self-assigned this Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/hook-handlers.ts:188 tags each Stop event with only the current scope, although parseHookEvent stores transcript-wide cumulative prompts, tokens, requests, and interventions on that event. After src/team-push.ts:454 filters first and aggregates second, a session with 3 user-scope prompts then 2 project prompts reports 3 to user and all 5 to the project. The new test omits transcript_path, so it does not exercise real Stop behavior.
  • [P1 blocking] src/team-push.ts:415 rejects every pre-upgrade event without dataHome when reporting the user scope because root is undefined. Any user-scope event recorded before upgrading but not yet represented in the shared snapshot is therefore never reported and is eventually lost to compaction; legacy events outside project roots must retain a user-scope path.
  • [P1 blocking] The PR description’s real-CLI record covers only Claude. The repository’s mandatory pre-PR matrix requires Claude, Codex, CodeBuddy, and OpenCode, in addition to the three providers. Add real-CLI verification records for Codex, CodeBuddy, and OpenCode before merge.

@jeff-r2026
jeff-r2026 merged commit 352cfc4 into Tencent:main Sep 24, 2026
11 checks passed
jeff-r2026 pushed a commit that referenced this pull request Sep 24, 2026
* fix(stats): await the async dashboard scope filter (#795)

#795 made filterEventsByScope async and changed its argument from a
projectRoot/excludeProjectRoots filter to the scope config, while #771
still called it synchronously with the old filter. On main, tsc fails in
stats.ts and every stats-scope test throws "events is not iterable";
`teamai stats` crashes once there are dashboard events.

stats now awaits the filter and passes the scope config, the call pull
makes, which is what #771 set out to do: show what the report sends.
The cwd-based project-root resolution is gone with the old argument.

The user-scope test followed pull's rule before #795 (keep events that
carry no dataHome); it now follows the current one: the user scope
never reports them, so it does not count them.

* fix(stats): subtract the scope's own reported snapshots (#786)

Since #795 each scope reports against its own reported-*.json under its
data home, and the shared ~/.teamai/dashboard files are no longer
written. stats still subtracted the shared files, so after the upgrade
every session reported since counted twice in the headline.

stats reads the snapshots through team-push's readers with the scope
config, including the one-time seed from the shared file.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 24, 2026
…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.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 24, 2026
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.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 24, 2026
…egacy entries (Tencent#785)

Seeding a scope's snapshot from the shared one kept only the runs still
in the log, so a session reported before Tencent#795 and compacted before the
scope's first pull was sent again in full when resumed. Only fallback
entries need that filter, against a reused PID; a tool's own session ID
is one session, so its entry is copied whole, as main did.

Splitting a bare entry across runs read the snapshot entry as typed,
and one without `tokens` (hand-edited or truncated) threw and skipped
the whole report; the prompt-token and intervention shares now parse it
as the owner-migration path already does.
jeff-r2026 pushed a commit that referenced this pull request Sep 25, 2026
…nst its own snapshots (#785, #786) (#791)

* fix(report): each scope reports only the dashboard sessions recorded in it (#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.

* fix(report): address pre-review findings (#785)

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

* docs(report): place the stats cleanup under usage reporting (#785)

The manual `stats/<user>.yaml` cleanup sat under single-repo mode, but the
pre-#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.

* docs(report): name where unattributed sessions go (#785)

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.

* fix(report): each scope keeps its own reported dashboard snapshots (#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.

* fix(report): report each dashboard session once, from the scope it started in (#785, #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 #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.

* fix(stats): read the scope's own dashboard filter and snapshots (#785, #786)

`teamai stats` (#771) still called filterEventsByScope with the old
{ projectRoot, excludeProjectRoots } options, synchronously, after #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 #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 (#785); the
stats-scope case that pinned "no exclusion in the user scope" now expects that.

* fix(stats): address CI review (#785)

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

* fix(stats): address CI review (#785)

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.

* fix(stats): address CI review (#785)

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.

* fix(stats): address CI review (#785)

adoptBareKeys() read a legacy bare `pid-N` snapshot entry as the first
run's, but only in memory: the success writes merge into the file, and
with nothing new to report nothing was written, so the bare entry stayed.
Once compaction dropped that run, the next run reusing `pid-N` read it and
was suppressed. The report now writes each snapshot as soon as a bare entry
is retired, under the run ID only, even when there is no delta.

* fix(stats): address CI review (#785)

A bare snapshot entry is given only to a run an earlier release recorded
(its first event has no dataHomeKey). Only earlier releases wrote bare
entries, and a seeded one may be another scope's run under a reused
PID-fallback ID, so a run this release recorded takes none. A marker of
the seed time would miss the common case: a scope seeds at its first
report, usually the pull its first session's SessionStart triggers.

A second end of a run with nothing recorded since the first (the
dashboard monitor's process_exit after SessionEnd) joins the run it
closed instead of opening a terminal-only run counted as a session.

* fix(stats): address CI review (#785)

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, so
a shared entry under the same ID is another scope's. An unmatched entry
is dropped instead of copied, so a later reuse of the ID cannot inherit
it.

The dashboard monitor records processExitAfter, the last event it
observed, and the scope filter closes only that run. A delayed exit
appended after the next run of the same ID began no longer ends it and
splits it in two; an exit whose run compaction dropped is ignored.

* fix(stats): address CI review (#785)

An earlier release summed every run of a reused ID under its bare
snapshot entry, but only the first retained run adopted it, so the next
one was reported again. Each of those runs in the log but the last is
now taken as reported at its own totals and the last takes the entry,
in the report, in teamai stats and in the seed from the shared file.
The last run is undercounted by at most the other runs' share, once.

A session_start on a fallback ID from another monitorPid than its open
run's begins a new run, so a run that crashed with no dashboard running
no longer takes the next invocation, maybe another scope's. A tool's own
ID is not split: Claude fires SessionStart again on resume, in a new
process, and its Stop carries the whole transcript.

* fix(stats): address CI review (#785)

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 counted it again, maybe in another scope.

* fix(stats): address CI review (#785)

A tool's own session ID is keyed by the ID itself again, as on main,
not by its first event's timestamp, so a session resumed after
compaction dropped its events still reads what its scope reported.
Only PID-fallback runs carry the timestamp.

A bare fallback entry is the sum of the runs of its ID in the log at
the earlier release's last report, and compaction keeps or drops an
ID's runs together. Those runs now consume it in log order, each up to
its own totals, so a later run that release never reported is sent
instead of taking the whole entry. The prompt-token snapshot decides
which runs it covered; interventions and daily follow it, and the first
run always takes a share.

* fix(stats): address CI review (#785)

Seeding a scope 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. The shared file summed every
scope's runs, so one scope consuming it alone could spend another
scope's baseline and suppress its own pending run.

The scope that first reports a tool's own session ID records itself in
~/.teamai/dashboard/session-owners.jsonl (the ID and its data home key,
no path), and a recorded session stays that scope's wherever it is
resumed, after compaction dropped its events too.

A dashboard started before processExitAfter existed reads the log and
appends its exit in one pass, so an unannotated exit less than one PID
check after the open fallback run began belongs to the run closed before
it instead of closing the next invocation.

* fix(stats): address CI review (#785)

A run taking its share of an earlier release's summed daily snapshot
keeps its own success and correction flags: the sum's are no single
run's (a successful run and an interrupted one sum to unsuccessful), so
an adopted run changed sessionsSucceeded without sessionsEnded.

An unannotated process_exit from a dashboard started before
processExitAfter no longer ends the open fallback run when more events
of that ID follow before the next start: a dead process records nothing
more, so it was observed before that run and belongs to the run closed
before it. This replaces the 15 s window, which a delayed callback or a
skewed clock could miss.

* fix(stats): address CI review (#785)

session-owners.jsonl is first written from the per-scope snapshots an
earlier release left: a tool's own ID in the user scope's or a
partition's prompt-token snapshot is that scope's, so a session main
reported in P, compacted and resumed in Q, stays P's instead of being
reported again to Q. An ID the shared snapshot also holds is left out:
main copied the shared file into every scope, so it names no owner, and
every scope already has its baseline. The file is created exclusively,
so a concurrent report in another scope reads the one written first.

* fix(stats): address CI review (#785)

Owner migration reconciles every per-scope baseline of an ID: a tool's
own ID in any of a scope's three snapshots is the scope's that holds its
greatest total (prompts, then tokens). A session main split per event
holds only part of it elsewhere, and a scope may have reported past the
shared total it was seeded with, so neither the first holder nor
leaving shared-held IDs out was right; a session reported with no
prompts, only its intervention count, is found too.

Besides the user scope and the partitions, it reads a project whose data
home is in its workspace that a session still in the log leads to, and
each report records the IDs of its own snapshots that no owner claims
yet, for such a project the log no longer leads to.

* fix(stats): address CI review (#785)

Owner migration assigns no owner when the greatest total ties across
scopes: main copied the shared snapshot into every scope, so equal
totals show only that copy, and each scope already holds the baseline.
A report records an ID of its own snapshots only when they show it
reported it (absent from the shared snapshot, or past its total there),
so a copy no longer claims it either.

A crashed fallback run a start from another process supersedes counts
as the run closed before it, so a late unannotated exit of it no longer
closes the new run.

* test(stats): pin a pre-upgrade exit reported before the next run's first prompt (#785)

The run split is recomputed from the whole log on every report, so once
the next run's first prompt follows the unannotated exit, the exit is
the earlier run's and the next run keeps its ID: the second pull
reports only its delta, not another session.

* fix(stats): give a compacted session resumed elsewhere to the project its transcript started in (#785)

Once compaction dropped every event of a project whose data home is in
its workspace, nothing outside it pointed to it, so a resume of its
Claude session in another project reported the transcript there again.
The transcript itself records where the session started: a Claude
transcript keeps its first cwd when resumed from another project (the
resume appends to the same file), as a Codex rollout keeps its
session_meta. Hooks now record transcriptPath on UserPromptSubmit and
SessionEnd as well as Stop (not SessionStart, whose path on such a
resume names a file that never exists; never Copilot's). A tool's own
session with no owner is the scope's that its origin resolves to, when
that scope's snapshots already hold it; otherwise it is decided as
before.

* fix(stats): address CI review (#785)

A session main split across scopes per event is credited once with
every part it reported: for each scope its `dataHome` names, the
shortest prefix of its events whose metrics reach its snapshot, and the
owner takes the metrics of their union as reported when they exceed its
own entry. Parts counted before any Stop carried the transcript's total
are no longer sent again by the owner, and cumulative Stops are not
credited twice.

A Copilot session with an explicit ID is traced to where it started by
Copilot's own session log, found by the session ID (TeamAI stores no
path of it, #666): its session.start context names the directory.

Compaction keeps a session whose tool process is still running, so a
run that an exit from a dashboard before processExitAfter marked stopped
keeps its start and its ID.

* fix(stats): address CI review (#785)

Owner migration takes a scope's entry as evidence only when its
snapshots show it reported the ID: the shared snapshots (interventions
included) hold none of it, or the scope is past their total. Main copied
the shared file into every scope it ran in, so a copy, even the only
one, names no owner, and the per-report recording follows the same rule.

A session main split across scopes whose events are gone is credited
from the parts' snapshots: a part whose daily entry shows a Stop holds
the transcript's cumulative total, so the greatest counts once; a part
with no Stop counted its own prompts, which add; intervention counts
add, tokens take the greatest. The credit rides on the owner's line in
session-owners.jsonl (numbers only) and is applied once as its baseline.

* fix(stats): keep a tool's own sessions in the first snapshot, parse legacy entries (#785)

Seeding a scope's snapshot from the shared one kept only the runs still
in the log, so a session reported before #795 and compacted before the
scope's first pull was sent again in full when resumed. Only fallback
entries need that filter, against a reused PID; a tool's own session ID
is one session, so its entry is copied whole, as main did.

Splitting a bare entry across runs read the snapshot entry as typed,
and one without `tokens` (hand-edited or truncated) threw and skipped
the whole report; the prompt-token and intervention shares now parse it
as the owner-migration path already does.

* fix(stats): report a resumed Codex rollout after compaction dropped the earlier one (#785)

A Codex build that writes a new rollout per resume restarts its
transcript counters, and the session summed only the rollouts still in
the log. Once compaction dropped rollout A, a resumed rollout B with
smaller counters was compared against A's reported total and reported
nothing until it passed it; routing the session back to the scope it
started in made that loss reach the resume in another scope too.

The prompt-token snapshot now keeps each rollout's reported prompts and
tokens under a hash of its path (no path stored), and a rollout that is
gone keeps its reported totals in the session's sum, so B is reported in
full. A session's prompts also sum its rollouts' Stop counts, which
restart per rollout like the tokens. An entry from before is compared
as a whole once, then kept per rollout.

* refactor(stats): move dashboard scope attribution and session owners out of team-push (#785)

No behavior change. src/dashboard-scope.ts holds which scope reports a
dashboard session (the log split into runs, each given whole to the
scope it started in, and the transcript origin); src/session-owners.ts
holds the machine-level owners index, its seeding from earlier
snapshots, and the snapshot files it reads. team-push.ts keeps the
snapshot adoption, deltas and push, and one reportedBaselines() now
serves both the report and `teamai stats`, which repeated the adoption
sequence.

* test(stats): real CLI resume of a compacted session from a workspace-data project (#785)

A non-git project W keeps its data home in its workspace. W reports a
Claude session; with no owners index and every W event compacted, the
session is resumed in git project Q through the real hook dispatcher,
appending to W's transcript. Q reports only its own session and W the
resumed turn; on a build without the transcript origin, Q reports both.
The fixture gains a second project and hooks sent as the installed
ones send them.

* fix(stats): credit a split session's Stop-derived interventions once (#785)

Interruptions and tool rejections come from Stops, which carry the
transcript's cumulative counts, so a compacted split session's credit
takes the greatest part, as it does for tokens; summing them made the
next cumulative Stop report nothing. Corrections are counted per prompt
in each part's own events, so they still add.

* fix(stats): place a compacted split session's parts by its transcript (#785)

A split session's credit added a part with no Stop to the greatest
cumulative Stop, which already counts that part when it came before the
Stop: after 3 prompts in P and a cumulative Stop of 5 in Q it credited
8, and the next Stop of 6 reported nothing. The credit now keeps each
part (scope key, prompts, whether it ended in a Stop; numbers only), and
the owner places them by the session's transcript, which keeps every
prompt in order with the directory it was typed in: the Stop covers the
first prompts, and only the part's prompts after those add. With no
transcript to place them, they all add, as before.

The Stop scan's human-turn test is now isHumanPromptEntry, shared by
both, so the two count prompts alike.

* fix(stats): keep a dropped Codex rollout's totals for daily and interventions, and migrate whole entries (#785)

An entry from before rollouts were kept is one total. An earlier
release rewrote every session in the log on each report, so it covers
the rollouts begun by the time its file was last written, read before
this report writes it: those still in the log consume it in order, what
is left is the dropped rollouts', kept as one prior rollout, and a
rollout begun later is new. Rollout B after a compacted A is no longer
compared against A's total and lost.

A rollout also keeps its Stop's interruptions and rejections, and its
dropped totals now reach the intervention and daily sums too, not just
prompts and tokens: daily prompt turns and intervention counts of a
resumed rollout were compared against the dropped one's.

* fix(stats): keep every metric of a dropped Codex rollout, with or without tokens (#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.

* fix(stats): sum a Codex session's rollout costs and keep a dropped rollout's failure (#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.

* fix(stats): keep modern Codex rollouts, and their submit-counted prompts, per rollout (#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.

* fix(stats): no legacy tokens on a spanning Codex counter; teamai stats writes no seed (#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.

* fix(stats): read a legacy daily entry's session cost fields as its day's costs (#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.

* fix(stats): keep every Codex variant per rollout, and an older Stop's request cost (#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.

* fix(stats): keep a Codex rollout's latest Stop by timestamp (#785)

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.

* fix(stats): an entry from before covers a running Codex rollout only as far as it had got (#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.

* fix(stats): credit a split session counter by counter; read an old entry's cutoff before its push (#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.
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

2 participants