Skip to content

feat(activity): correlate capa sh traces to provider conversations - #173

Open
Minitour wants to merge 5 commits into
developfrom
feature/activity-output-fingerprint-correlation
Open

feat(activity): correlate capa sh traces to provider conversations#173
Minitour wants to merge 5 commits into
developfrom
feature/activity-output-fingerprint-correlation

Conversation

@Minitour

@Minitour Minitour commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Link capa sh MCP traces to provider afterShell hooks by parsing the shell command (kebab-case ↔ qualified tool names) and time window.
  • Keep conversation inheritance for direct MCP tool calls; stop inheriting at start for capa sh shell traces to avoid wrong chat grouping.
  • Hide uncorrelated capa traces from the activity API/SSE and UI feed so failed links do not appear as orphan Activity junk.
  • Reconcile linked capa rows to Cursor composer chat ids; sticky conversation section headers and a load-more footer outside the scroll area.

Test plan

  • npm test — activity correlate, capa-sh command, feed visibility, tool-call-tracer
  • Restart capa server, run wrap agent with capa sh tools — capa spans appear under the same conversation as the prompt
  • Confirm uncorrelated capa traces do not show in the feed
  • Scroll activity list — conversation headers stick; load more does not overlap rows

Made with Cursor

Pair capa shell spans with provider afterShell hooks using parsed capa sh
commands and time windows (output fingerprint as fallback), keep MCP
inheritance for direct tool calls, hide uncorrelated capa rows from the UI,
and add iOS-style sticky conversation headers with a fixed load-more footer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Correlate capa sh shell traces to provider conversations and hide orphans

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Correlate capa sh traces to provider afterShell hooks via command/time, fingerprint fallback.
• Stop inheriting “latest conversation” for shell traces; patch correlation after matching.
• Filter uncorrelated capa traces from API/SSE and UI; add sticky conversation headers.
Diagram

graph TD
  A{{"Provider afterShell"}} --> B["Activity API/SSE"] --> C["ToolCallTracer"] --> D[("tool_calls")]
  C --> E["Trace correlator"] --> D
  D --> B
  B --> F["Feed visibility"] --> G["Activity UI"]

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc["Service"] ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Pass an explicit correlation token through `capa sh`
  • ➕ Deterministic linking; avoids ambiguous command/time matching
  • ➕ No reliance on output equality or time windows
  • ➖ Requires changes to the capa shell wrapper/client protocol and provider hook integration
  • ➖ More coupling between provider runtime and capa CLI invocation
2. Correlate via a shared session/run id persisted at start
  • ➕ Simpler than parsing commands; stable even if output differs
  • ➕ Enables straightforward SQL joins/indexing
  • ➖ May not exist across process boundaries for shell-invoked traces
  • ➖ Requires ensuring the provider hook and shell tracer share/propagate the same id
3. Background reconciliation job for orphaned shell traces
  • ➕ Decouples ingestion paths; can retry with richer context
  • ➕ Can handle ordering issues where hook/trace arrive far apart
  • ➖ Adds operational complexity (scheduler/queue) and eventual-consistency UX
  • ➖ Still needs a robust matching signal (token, command, fingerprint, etc.)

Recommendation: The PR’s approach is reasonable for the current constraints (no explicit shared correlation id): prefer command+time matching for capa sh, with output fingerprint as a fallback, and aggressively hide uncorrelated capa rows to avoid UI noise. If correlation accuracy becomes critical or ambiguity increases, the next step should be introducing an explicit correlation token/run id passed through the capa sh wrapper so linking is deterministic and doesn’t depend on heuristics.

Files changed (21) +1295 / -48

Enhancement (13) +774 / -42
database.tsExpose repo helpers for trace correlation and late patching +45/-0

Expose repo helpers for trace correlation and late patching

• Adds database-level methods to patch tool_call correlation and query provider/capa rows by fingerprint and time windows, enabling the correlator to link previously uncorrelated traces.

src/db/database.ts

tool-calls.tsPersist output fingerprints and add correlation/query primitives +127/-3

Persist output fingerprints and add correlation/query primitives

• Adds 'result_fingerprint' to insert/finish flows, introduces 'patchCorrelation', and implements SQL queries to find provider hooks and capa traces for linking by fingerprint or 'capa sh' detection within time windows.

src/db/tool-calls.ts

activity-routes.tsAttempt correlation when provider shell hook finishes +11/-1

Attempt correlation when provider shell hook finishes

• Captures the finished provider shell hook row and tries to link an uncorrelated capa shell trace; if linked, it triggers a follow-up notification for the patched capa record.

src/server/activity-routes.ts

project-routes.tsFilter activity API/SSE output to hide uncorrelated capa traces +12/-1

Filter activity API/SSE output to hide uncorrelated capa traces

• Adds a visibility gate for SSE notifications and filters activity page responses so uncorrelated capa-originated rows do not appear as orphan feed entries.

src/server/project-routes.ts

tool-call-tracer.tsCompute output fingerprints and perform late correlation on finish +34/-9

Compute output fingerprints and perform late correlation on finish

• Stops inheriting correlation for 'source: shell' at start, computes 'result_fingerprint' from result previews, attempts post-finish correlation for capa shell traces, and provides a 'notifyRecord' helper for late-patched broadcasts.

src/server/tool-call-tracer.ts

activity-capa-sh-command.tsParse 'capa sh' argv and match to qualified tool names +90/-0

Parse 'capa sh' argv and match to qualified tool names

• Implements parsing of 'capa sh …' segments from provider hook shell text (including args_json command fields) and matching logic from kebab-case segments to slugified qualified tool names.

src/shared/activity-capa-sh-command.ts

activity-feed-visible.tsCentralize visibility rules for activity feed rows +44/-0

Centralize visibility rules for activity feed rows

• Adds shared helpers to identify capa-originated rows and to hide those lacking a provider correlation, plus list filtering for both server and UI consumers.

src/shared/activity-feed-visible.ts

activity-output-fingerprint.tsAdd stable output fingerprinting and hook classification helpers +92/-0

Add stable output fingerprinting and hook classification helpers

• Introduces SHA-256 fingerprinting over normalized output prefixes, time-window utilities, and helpers to detect provider shell hooks and 'capa sh' wrapper invocations.

src/shared/activity-output-fingerprint.ts

activity-trace-correlate.tsImplement correlation engine for 'capa sh' traces ↔ provider hooks +254/-0

Implement correlation engine for 'capa sh' traces ↔ provider hooks

• Adds the core matching logic: prefer 'capa sh' command segment matching within a time window (with ambiguity checks), and fall back to fingerprint matching when outputs align; patches correlation into persisted rows.

src/shared/activity-trace-correlate.ts

database.tsExtend ToolCallRecord with 'result_fingerprint' +2/-0

Extend ToolCallRecord with 'result_fingerprint'

• Updates the ToolCallRecord type to include the persisted output fingerprint used for correlation.

src/types/database.ts

ActivityFeed.tsxSticky conversation headers and fixed load-more footer +51/-27

Sticky conversation headers and fixed load-more footer

• Updates the activity feed layout to use a scroll container with sticky column and section headers, shortens displayed conversation ids, and moves the load-more footer outside the scroll area to avoid overlap.

web-ui/src/features/projects/components/activity/ActivityFeed.tsx

groupActivityRuns.tsFilter hidden activity rows before grouping conversations +4/-1

Filter hidden activity rows before grouping conversations

• Applies shared visibility filtering before reconciling and grouping activity rows, preventing orphaned capa traces from forming bogus conversation blocks.

web-ui/src/features/projects/components/activity/groupActivityRuns.ts

hooks.tsDrop uncorrelated capa traces from live SSE state +8/-0

Drop uncorrelated capa traces from live SSE state

• Uses shared visibility rules when merging incoming tool-call SSE events, removing previously inserted uncorrelated capa rows and preventing them from lingering in client state.

web-ui/src/features/projects/hooks.ts

Bug fix (1) +0 / -1
activity-correlation-reconcile.tsBroaden conversation-id reconciliation beyond cursor-only rows +0/-1

Broaden conversation-id reconciliation beyond cursor-only rows

• Removes the cursor-only guard so conversation reconciliation can rewrite additional row sources (e.g., capa shell traces) based on agent-session-to-chat mappings.

src/shared/activity-correlation-reconcile.ts

Tests (6) +519 / -5
tool-call-tracer.test.tsTest capa shell correlation behavior and SSE filtering +166/-5

Test capa shell correlation behavior and SSE filtering

• Updates inheritance semantics tests (MCP inherits; shell does not), adds tests for linking via fingerprint/command mismatch scenarios, and verifies SSE does not emit uncorrelated capa shell traces.

src/server/tests/tool-call-tracer.test.ts

activity-capa-sh-command.test.tsValidate 'capa sh' command parsing and tool-name matching +39/-0

Validate 'capa sh' command parsing and tool-name matching

• Adds unit tests for slugifying qualified tool names and parsing/matching kebab-case CLI segments back to snake_case qualified tool ids.

src/shared/tests/activity-capa-sh-command.test.ts

activity-correlation-reconcile.test.tsReconcile linked capa shell traces to chat conversation ids +33/-0

Reconcile linked capa shell traces to chat conversation ids

• Adds coverage ensuring capa shell traces linked to agent session ids are rewritten to the actual composer chat conversation id.

src/shared/tests/activity-correlation-reconcile.test.ts

activity-feed-visible.test.tsTest visibility rules for uncorrelated capa rows +46/-0

Test visibility rules for uncorrelated capa rows

• Introduces tests verifying that uncorrelated capa shell/tool rows are hidden while provider hook rows remain visible, and that list filtering behaves as expected.

src/shared/tests/activity-feed-visible.test.ts

activity-output-fingerprint.test.tsTest stable output fingerprinting and capa-sh detection +44/-0

Test stable output fingerprinting and capa-sh detection

• Covers newline normalization, deterministic hashing for empty output, provider 'capa sh' detection in tool_name/args_json, and prefix-based hashing behavior for long outputs.

src/shared/tests/activity-output-fingerprint.test.ts

activity-trace-correlate.test.tsEnd-to-end tests for trace correlation heuristics +191/-0

End-to-end tests for trace correlation heuristics

• Adds comprehensive tests for ambiguity rejection, command-based matching, fingerprint fallback matching, and selection preferences for nearest-in-time candidates.

src/shared/tests/activity-trace-correlate.test.ts

Other (1) +2 / -0
schema.tsAdd 'result_fingerprint' column to tool_calls +2/-0

Add 'result_fingerprint' column to tool_calls

• Extends the tool_calls schema with a 'result_fingerprint' column and ensures it exists during schema initialization/migration.

src/db/schema.ts

Satisfy ToolCallRecord after the new column so tsc --noEmit passes in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Activity paging dead-end ✓ Resolved 🐞 Bug ≡ Correctness
Description
handleGetProjectActivity filters out invisible rows after paging, but the UI refuses to load more
when the returned calls array is empty, so users can get stuck with no activity even when older
visible rows exist. This happens when the newest page is entirely uncorrelated capa rows that are
now hidden.
Code

src/server/project-routes.ts[R543-546]

+		JSON.stringify({
+			...page,
+			calls: filterVisibleActivityFeed(page.calls),
+		}),
Evidence
The server now filters page.calls in the HTTP response, but hasMore is computed over unfiltered
rows; the UI loadMore callback hard-stops when calls.length === 0, so an empty filtered page
cannot be recovered from even if hasMore is true.

src/server/project-routes.ts[517-548]
src/db/tool-calls.ts[453-518]
web-ui/src/features/projects/hooks.ts[235-256]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handleGetProjectActivity` filters `page.calls` after pagination and returns the filtered list without ensuring it is non-empty when `hasMore` is true. The UI’s `loadMore` is guarded by `calls.length === 0`, so an empty first page makes it impossible to paginate to older visible rows.
### Issue Context
Filtering needs to participate in pagination (or the server must keep fetching until it has at least one visible call or reaches the end) so clients always have a cursor.
### Fix Focus Areas
- src/server/project-routes.ts[517-548]
- src/db/tool-calls.ts[453-518]
- web-ui/src/features/projects/hooks.ts[235-256]
### Suggested fix
- On the server, change `handleGetProjectActivity` to ensure it never returns `calls: []` while `hasMore: true`.
- Option A (preferred): implement a DB-level “visible activity” listing method that applies the same visibility predicate in SQL and computes `hasMore` using the visible cursor.
- Option B: in `handleGetProjectActivity`, iteratively fetch additional pages using the *unfiltered* oldest row as the cursor, append, then filter, until you accumulate N visible rows (or no more rows). Update `hasMore` based on whether there remain any rows before the final unfiltered cursor.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Fingerprint mis-correlation risk ✓ Resolved 🐞 Bug ≡ Correctness
Description
The fingerprint fallback can link a capa shell trace to a non-capa sh provider shell hook if it’s
the only fingerprint match in the time window, corrupting conversation_id/generation_id. This is
especially likely for empty/near-empty outputs where the fingerprint is a constant and collisions
are common.
Code

src/shared/activity-trace-correlate.ts[R59-62]

+	const capaSh = candidates.filter((row) => providerShellLooksLikeCapaSh(row));
+	if (capaSh.length === 1) return capaSh[0]!;
+	if (capaSh.length > 1) return null;
+	if (candidates.length === 1) return candidates[0]!;
Evidence
The correlation logic explicitly falls back to returning a single non-capa sh candidate, and the
DB query that supplies those candidates is not restricted to capa sh provider hooks. The
fingerprint function hashes null/undefined to an empty string, making collisions much more likely
for empty outputs.

src/shared/activity-trace-correlate.ts[56-64]
src/shared/activity-trace-correlate.ts[174-217]
src/db/tool-calls.ts[170-196]
src/shared/activity-output-fingerprint.ts[18-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fingerprint-based fallback (`tryLinkByOutputFingerprintFromCapaTrace` / `tryLinkByOutputFingerprintFromProviderHook`) queries *all* provider shell hooks by fingerprint, and `pickUniqueCapaShProviderShellMatch` will accept a single candidate even when it does **not** look like `capa sh`. This can assign the wrong conversation/generation to capa traces.
### Issue Context
The PR intent is to correlate `capa sh` traces to provider *wrapper* hooks. Fingerprint matching should be constrained to provider hooks that actually represent `capa sh` wrappers (and ideally also match the tool name when available).
### Fix Focus Areas
- src/shared/activity-trace-correlate.ts[56-217]
- src/db/tool-calls.ts[170-196]
- src/shared/activity-output-fingerprint.ts[18-70]
### Suggested fix
- Make fingerprint fallback only consider provider hooks that are `capa sh` wrappers:
- Either: change `findProviderShellHooksForFingerprint` to include a `capa sh` predicate (same as `findProviderCapaShHooksInWindow`) so non-`capa sh` provider hooks can’t be selected.
- And/or: change `pickUniqueCapaShProviderShellMatch` to **not** return `candidates[0]` when `capaSh.length === 0` (i.e., require `providerShellLooksLikeCapaSh`).
- Add a guard to avoid linking on empty output fingerprints (e.g., if normalized output is empty/whitespace, skip fingerprint fallback) to reduce collision-driven mislinks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Redirect breaks capa sh parse ✓ Resolved 🐞 Bug ≡ Correctness
Description
parseCapaShSegmentsFromShellText does not treat common redirections like 2>/dev/null as a stop
token, so it can incorrectly parse 2 as an argv segment and fail command-based correlation. This
will leave valid capa traces uncorrelated (and therefore hidden).
Code

src/shared/activity-capa-sh-command.ts[R41-44]

+		rest = rest.trimStart();
+		if (!rest || rest.startsWith("--")) break;
+		if (/^2>$|^>&|^[|;&]/.test(rest)) break;
+
Evidence
The current guard only matches 2> when it is the entire remaining string, and the token regex will
accept 2 as a valid segment, yielding incorrect segments.

src/shared/activity-capa-sh-command.ts[40-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stop condition in `parseCapaShSegmentsFromShellText` only matches a standalone `2>` (`/^2>$/`), so `2>file` / `2>/dev/null` are not recognized. The tokenizer then consumes `2` as a segment and stops at `>`, producing the wrong segment list.
### Issue Context
Provider hook commands may include redirections, especially when wrappers suppress stderr. Correlation by command should not be derailed by redirection syntax.
### Fix Focus Areas
- src/shared/activity-capa-sh-command.ts[33-53]
### Suggested fix
- Replace the redirection/operator guard with something that catches common redirections at the start of `rest`, e.g.:
- `if (/^(?:\d*>|>&|>>|<|[|;&])/.test(rest)) break;`
- Or at minimum `if (/^\d*>/.test(rest)) break;` plus `if (/^>/.test(rest)) break;`.
- Add a unit test covering `capa sh … 2>/dev/null` parsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Fingerprint exposed to clients ✓ Resolved 🐞 Bug ⛨ Security
Description
result_fingerprint is computed from the raw (pre-redaction) resultPreview and persisted, then
included in SSE and activity API payloads via JSON.stringify(record). While not reversible, this
exposes a deterministic derived identifier that can enable equality/dictionary checks over sensitive
outputs.
Code

src/server/tool-call-tracer.ts[R157-160]

if (input.resultPreview !== undefined) {
+			resultFingerprint = fingerprintActivityOutput(input.resultPreview);
  const sized = serializeResultWithSize(input.resultPreview, secrets);
  resultPreview = sized.preview;
Evidence
The fingerprint is computed from the raw resultPreview before redaction, stored on the record, and
ToolCallRecord objects are sent directly to clients via SSE and the activity history response.

src/server/tool-call-tracer.ts[145-193]
src/server/project-routes.ts[496-507]
src/server/project-routes.ts[517-548]
src/types/database.ts[103-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ToolCallTracer.finish` computes `result_fingerprint` from the unredacted `resultPreview` and stores it on the tool_calls row. The server then serializes full `ToolCallRecord` objects to browsers (SSE + activity history), which now includes `result_fingerprint`.
### Issue Context
Even though SHA-256 is not reversible, exposing a stable hash derived from sensitive output can allow equality checks and dictionary attacks when the output has low entropy. Correlation can still use the fingerprint server-side without sending it to clients.
### Fix Focus Areas
- src/server/tool-call-tracer.ts[145-193]
- src/server/project-routes.ts[496-548]
- src/types/database.ts[103-113]
### Suggested fix
- Keep `result_fingerprint` in the DB/server model, but strip it from client-facing payloads:
- In `notifyToolCall`, send a sanitized object (e.g., `{ ...record, result_fingerprint: null }` or omit the field).
- In `handleGetProjectActivity`, map `calls` to a public DTO that omits `result_fingerprint` before JSON serialization.
- (Optional) introduce `ToolCallRecordPublic = Omit<ToolCallRecord, 'result_fingerprint'>` to make the boundary explicit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/server/project-routes.ts Outdated
Comment thread src/shared/activity-trace-correlate.ts
Comment thread src/shared/activity-capa-sh-command.ts
Comment thread src/server/tool-call-tracer.ts
Minitour and others added 3 commits August 5, 2026 21:25
Rely on capa sh command + time matching only; uncorrelated capa shell traces stay hidden from the feed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip hidden capa pages when listing activity history, and stop capa sh argv parsing on 2>/dev/null-style redirections.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use top-level await in the CLI entrypoint so Bun does not exit while Bun.stdin.text() is pending, emit gate JSON before ingest, and bound stdin reads so Cursor hooks cannot hang.
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