Skip to content

feat: concurrency — how many sessions ran at once, and what it bought - #32

Merged
pa-arth merged 3 commits into
mainfrom
feat/concurrency
Aug 18, 2026
Merged

feat: concurrency — how many sessions ran at once, and what it bought#32
pa-arth merged 3 commits into
mainfrom
feat/concurrency

Conversation

@pa-arth

@pa-arth pa-arth commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Adds the third axis to cc-audit. Tokens first, wall-clock second, now concurrency: how many sessions were alive at the same time, and what the overlap bought.

Two totals over the same minutes — agentMinutes (each session's active minutes summed) and wallMinutes (their union) — whose ratio is the mean number of sessions live whenever anything was live.

The load-bearing change is parentSessionId

The loader makes one Session per file, and Claude Code 2.1.x moved subagent turns into <session>/subagents/agent-*.jsonl. That is correct for spend, where message-id dedup keeps the total honest, and wrong for anything counting sessions: a parent with six subagents reads as seven concurrent sessions. concurrencyKey() folds a subagent into its parent, and a test asserts the naive version says 7 where the correct one says 1.

Three things the profile reports that a single number would hide

  • sensitivity sweeps the bridge threshold (1/2/5/10 min) instead of reporting one flattering value. The bridge — how long a gap still counts as working — is the method's only free parameter. A finding that dies inside its plausible range was never a finding.
  • sessionWeightedMean alongside meanConcurrent. The first is how crowded it is from inside a running session, the second is what the operator experiences. On the author's corpus they are 3.8 and 2.7, and they answer different questions.
  • soloShare next to multiShare, and the card says outright which it is. Solo is routinely the tallest single bar while being a minority of the time; a plurality read as a majority is the standing misreading of this distribution.

Both prompt rates print side by side rather than one being chosen. Per agent hour asks whether each session needs less of you when there are more of them; per wall hour is what the day feels like. They move in opposite directions (6.4 → 5.1 and 6.4 → 44.1 here), so either alone tells half the story. The steering verdict is derived from the ratio, so a corpus where attention does not hold flat gets the opposite sentence rather than the flattering one.

Idle time is excluded rather than counted as zero: this measures how many ran when anything ran, not an average over the calendar. Prompts counted for steering skip sidechain spans — a subagent's task instruction is the machine talking to itself, not the operator steering.

Report

╭─ CONCURRENCY  ·  how many ran at once, and what it bought ───────────────────╮
│ 2.7× average — sessions live whenever anything was live                      │
│ 180.2h of your time carried 484.9h of agent work (+304.6h)                   │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄       │
│ live     yours    agent  share              /agent-hr                        │
│ 1        58.8h    58.8h  █████████████  33%       6.4                        │
│ 2        41.8h    83.6h  █████████▎     23%       6.9                        │
│ 3-4      51.0h   174.8h  ███████████▍   28%       7.2                        │
│ 5-7      25.6h   141.8h  █████▊         14%       6.6                        │
│ 8+        3.0h    25.9h  ▊               2%       5.1                        │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄       │
│ solo 33% of your time · two or more 67%  — the tallest bar, still a minority │
│ inside a session it is busier: 3.8× · busiest minute 13 · median 2           │
│ steering 6.4 → 5.1 prompts per agent-hour (1 → 8+ live)                      │
│   flat: parallelism buys calendar time, not attention                        │
│ out of you: 6.4 → 44.1 prompts per hour of YOUR time                         │
│ bridge 1m/2m/5m/10m → 2.4 / 2.5 / 2.7 / 2.9× (5m used above)                 │
╰──────────────────────────────────────────────────────────────────────────────╯

The card is gated on two sessions actually existing — with one transcript the question has no answer, and printing 1.0× would read as a finding.

Privacy

Every field is a count or a ratio: no paths, project labels, or session ids. It is nonetheless not in the uploaded aggregate — putting it there is a schema-version decision, not a rendering one, so it stays local until that call is made deliberately.

Verification

  • npm run typecheck clean
  • npm run lint clean
  • npx vitest run42 files / 434 tests passed (20 of them new)

One test asserts every rendered line is exactly the card width. That is not decoration: three lines overflowed the frame the first time this ran against a real corpus, and card() pads an underflow while silently letting an overflow break the right wall.

🤖 Generated with Claude Code

Adds the third axis to cc-audit: tokens first, wall-clock second, now
concurrency. Two totals over the same minutes — agentMinutes (each session's
active minutes summed) and wallMinutes (their union) — whose ratio is the mean
number of sessions live whenever anything was live.

The load-bearing change is `parentSessionId`. The loader makes one Session per
FILE, and Claude Code 2.1.x moved subagent turns into
`<session>/subagents/agent-*.jsonl`. That is correct for spend, where message-id
dedup keeps the total honest, and wrong for anything counting sessions: a parent
with six subagents reads as seven concurrent sessions. `concurrencyKey()` folds
a subagent into its parent, and a test asserts the naive version says 7 where
the correct one says 1.

Three things the profile reports that a single number would hide:

- `sensitivity` sweeps the bridge threshold (1/2/5/10 min) rather than reporting
  one flattering value. The bridge is the only free parameter in the method; a
  finding that dies inside its plausible range was never a finding.
- `sessionWeightedMean` alongside `meanConcurrent`. The first is how crowded it
  is from inside a running session, the second is what the operator experiences.
  They differ by a lot (2.7 vs 3.8 on the author's corpus) and answer different
  questions.
- `soloShare` is reported next to `multiShare` because solo is routinely the
  tallest single bar while being a minority of the time — a plurality read as a
  majority is the standing misreading of this distribution.

Idle time is excluded rather than counted as zero: this measures how many ran
when anything ran, not an average over the calendar. Prompts counted for
`steering` skip sidechain spans — a subagent's task instruction is the machine
talking to itself, not the operator steering.

Every field is a de-identified aggregate (counts and ratios, no paths, project
labels or session ids), so the profile is uploadable under the local-first rule.
Wires computeConcurrency into runAudit and gives it a section between FLUENCY
and the context knee. Gated on two sessions actually existing — with one
transcript the question has no answer and the card would print 1.0x as if it
were a finding.

The card prints the SHAPE, not just the average, because the average is the
part that misleads: solo is routinely the tallest single bar while being a
minority of the time, and a reader handed only "2.7x" reads it as "mostly
alone". The per-level table makes that visible, and the solo line says outright
whether it is a majority or merely the tallest bar.

Both prompt rates are printed side by side rather than one being chosen. Per
AGENT hour answers whether each session needs less of you when there are more
of them; per WALL hour is what the day feels like. On the author's corpus they
move in opposite directions (6.4 -> 5.1 and 6.4 -> 44.1), so either alone tells
half the story. The steering verdict is derived from the ratio rather than
asserted, so a corpus where attention does NOT hold flat gets the opposite
sentence instead of the flattering one.

The bridge sweep is printed for the same reason it is computed: it is the
method's only free parameter, and a reader cannot tell a finding from an
artifact of one threshold without seeing the range.

A test asserts every line is exactly the card width. That is not decoration —
three lines overflowed the frame the first time this rendered against a real
corpus, and card() pads an underflow while silently letting an overflow break
the right wall.

The profile stays OUT of the uploaded aggregate. Every field is a count or a
ratio and would be safe there, but adding it is a schema-version decision, not
a rendering one.
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds a local concurrency profile that folds subagent transcripts into parent sessions and reports overlap, steering rates, concurrency distribution, and bridge sensitivity.

  • Adds minute-level concurrency aggregation and sensitivity calculations.
  • Adds parent-session extraction for Claude Code subagent transcripts.
  • Integrates the profile into audit results, public exports, and report rendering.
  • Adds focused computation, rendering, and wiring tests.

Confidence Score: 4/5

The Windows parent-session parsing defect should be fixed before merging because it defeats the feature’s key subagent-folding guarantee and materially inflates concurrency reports.

Native Windows paths reach the new slash-only parent parser unchanged, so subagent transcripts lose their parent relationship and are counted as separate concurrent sessions.

Files Needing Attention: src/adapters/claudeCode.ts

Important Files Changed

Filename Overview
src/adapters/claudeCode.ts Adds parent-session extraction, but its POSIX-only path parsing misses subagent relationships on Windows.
src/concurrency.ts Implements minute-level concurrency, bridge sensitivity, distribution, and steering calculations with parent-session folding.
src/model.ts Extends Session with parentSessionId and provides the grouping key used by concurrency calculations.
src/audit.ts Computes and exposes the concurrency profile as part of every audit result.
src/report.ts Adds a gated concurrency card with distribution, steering, and sensitivity output.
src/tests/concurrency.test.ts Covers core calculations and rendering but tests parent extraction only with POSIX-style paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Claude Code transcript files] --> B[parseTranscript]
  B --> C[parentSessionId]
  C --> D[concurrencyKey]
  D --> E[Group turns by owning session]
  E --> F[Build active-minute map]
  F --> G[Compute concurrency and steering profile]
  G --> H[Render concurrency card]
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "feat(report): print the concurrency card" | Re-trigger Greptile

Comment thread src/adapters/claudeCode.ts Outdated
Comment on lines +454 to +458
const parts = filePath.split('/');
// .../<parentSessionId>/subagents/<agent>.jsonl
return parts.length >= 3 && parts[parts.length - 2] === 'subagents'
? (parts[parts.length - 3] ?? null)
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Windows parent-session links break

When an audit runs on Windows, native backslash-delimited subagent paths reach parentOf unchanged, but splitting only on / returns no parent. Each subagent is therefore counted as an independent session, inflating the reported session count, peak, mean concurrency, and minutes bought.

Suggested change
const parts = filePath.split('/');
// .../<parentSessionId>/subagents/<agent>.jsonl
return parts.length >= 3 && parts[parts.length - 2] === 'subagents'
? (parts[parts.length - 3] ?? null)
: null;
const parts = filePath.split(/[\\/]/);
// .../<parentSessionId>/subagents/<agent>.jsonl
return parts.length >= 3 && parts[parts.length - 2] === 'subagents'
? (parts[parts.length - 3] ?? null)
: null;

Fix in Claude Code Fix in Cursor

Greptile P1. The loader builds paths with join() from node:path, which emits
backslashes on Windows, so splitting on '/' alone returned a single segment
there — parentOf found no parent and every subagent became an independent
top-level session, inflating session count, peak, mean concurrency and minutes
bought by however many subagents the run spawned.

Fixed at both sites rather than only the one the review flagged: `sessionId`
had the identical defect on line 150 and predates this branch, where it turned
the whole absolute path into the session id. One helper now owns the split, so
a third caller cannot reintroduce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pa-arth
pa-arth merged commit 3c2acdb into main Aug 18, 2026
2 checks passed
@pa-arth
pa-arth deleted the feat/concurrency branch August 18, 2026 00:43
@pa-arth pa-arth mentioned this pull request Aug 18, 2026
pa-arth added a commit that referenced this pull request Aug 18, 2026
One merged PR since 0.8.0 (#32), and it adds a measurement rather than correcting
one — so unlike 0.8.0 there is no number a user has already read that changes
meaning here. MINOR: new public surface (`computeConcurrency`, `renderConcurrency`,
`concurrencyKey`, `Session.parentSessionId`, a `concurrency` field on
`AuditResult`), all additive, aggregate schemaVersion untouched at 10.

WHAT MAKES IT A RELEASE. cc-audit could say what sessions cost and how long they
ran, and nothing about how many ran at the same time. For anyone running more than
one that single fact reframes the rest of the report: 180h of wall time carrying
487h of agent work is a different product than 487h of work.

THE FIX THAT PREDATES THE FEATURE. Transcript paths were split on '/' only, while
the loader builds them with join() from node:path. On Windows that returns one
segment, so the whole absolute path became the sessionId — shipped in every prior
version — and with the new parent link every subagent would have counted as an
independent session. Both sites now share one helper.

VERIFIED, not assumed:
- npm ci && lint && typecheck clean; 42 test files / 435 tests pass.
- Built the npm artifact and confirmed bundles/npm/package.json reads 0.9.0, so
  publish.yml's tag-vs-package guard will match v0.9.0.
- Ran the BUNDLE against the real local corpus, not just --help: 2.7x average,
  180.7h wall carrying 486.7h agent, peak 13, bridge sweep 2.4/2.5/2.7/2.9 — the
  section renders inside its frame at every row, which is the failure this section
  actually has (three lines overflowed the card the first time it met real data).

Tag v0.9.0 after merge; .github/workflows/publish.yml guards tag == package.json
and publishes + attaches the standalone binaries.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant