Conversation
Per-plugin errors become warnings; healthy plugins still expand into skills/servers for install and the web UI. Co-authored-by: Cursor <cursoragent@cursor.com>
Clear lock pins, merged expansions, and unpack dirs so a late error cannot leave a half-installed plugin while reporting it as skipped. Co-authored-by: Cursor <cursoragent@cursor.com>
…lures fix: isolate plugin resolve failures
Install snippets now pin ::subpath (and optional gitRef) from the marketplace API, and resolvePlugins falls back to finding a nested plugin by id when the repo root only has a catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the full-tree findPluginInDirectory fallback with a direct-child and marketplace-catalog resolve, and fix the adapter test import for tsc. Co-authored-by: Cursor <cursoragent@cursor.com>
…subpath fix: resolve Cursor marketplace monorepo plugins
Read SKILL.md from the unpacked plugin tree under ~/.capa/plugins so the UI can show content as soon as a plugin is added, without requiring install to materialize provider skill dirs. Co-authored-by: Cursor <cursoragent@cursor.com>
Prefer provider-installed skill copies over the unpacked plugin tree so post-install sanitized content wins, and cache plugin skill directory scans by mtime to avoid repeated walks. Co-authored-by: Cursor <cursoragent@cursor.com>
…e-install Fix plugin skill content loading before capa install
beforeFileRead and other gate events require valid allow/continue JSON; emit it in a finally so ingest stays fail-open without failing closed. Co-authored-by: Cursor <cursoragent@cursor.com>
Cursor already records reads via afterTool/postToolUse; the beforeReadFile hook only duplicated them as kind file and risked gate failures. Co-authored-by: Cursor <cursoragent@cursor.com>
Add hooks.activityCorrelation field maps per provider so ingest extracts chat/turn ids without hardcoding, persist them on tool_calls, and nest the Activity feed conversation → generation → spans. Co-authored-by: Cursor <cursoragent@cursor.com>
Add hooks.activityAttributes field maps so model, model_id, versions, and related metadata are stored on tool_calls without provider-specific hardcoding. Co-authored-by: Cursor <cursoragent@cursor.com>
Read PostToolUse results from provider-configured fields (Claude tool_response, Cursor tool_output) instead of assuming Cursor-only names. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep generation rows focused on the turn; show the provider once per conversation. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Inherit conversation/generation on MCP traces and keep afterShell rows for capa sh so Bash/Shell calls are visible in the activity feed. Co-authored-by: Cursor <cursoragent@cursor.com>
Apply Biome format/import fixes across activity files and keep activity-ingest fail-open when stdout is broken. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix activity-ingest empty stdout blocking Cursor gate hooks
Ensure capa wrap starts the server on warm reuse, strip UTF-8 BOM from Cursor Agent hook stdin on Windows, and stop provider hook events from inheriting another provider's conversation ids. Co-authored-by: Cursor <cursoragent@cursor.com>
Notify late SSE subscribers when the shared project EventSource is already open so the activity feed does not stay offline after capabilities sync connects first. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace wrapCommand module mocks that broke unrelated wrap tests, and build a client-connectable origin for wildcard/IPv6 server hosts. Co-authored-by: Cursor <cursoragent@cursor.com>
Fix wrap server start, Windows Cursor activity ingest, and live indicator
| ) { | ||
| return raw; | ||
| } | ||
| if (Array.isArray(raw) || (typeof raw === "object" && raw !== null)) { |
PR Summary by Qodov2.0.1: Correlated activity feed, resilient plugins, and wrap/ingest fixes
AI Description
Diagram
High-Level Assessment
Files changed (45)
|
Code Review by Qodo
1. Truncated attributes break JSON
|
| try { | ||
| const json = JSON.stringify(attributes); | ||
| if (json.length <= ACTIVITY_ATTRIBUTES_MAX_JSON_CHARS) return json; | ||
| return `${json.slice(0, ACTIVITY_ATTRIBUTES_MAX_JSON_CHARS - 1)}…`; |
There was a problem hiding this comment.
1. Truncated attributes break json 🐞 Bug ≡ Correctness
serializeActivityAttributes() slices the serialized JSON string and appends an ellipsis when over the size cap, which can produce non-JSON text stored in tool_calls.attributes_json. Any consumer that JSON.parse()s attributes_json will throw once a large enough attribute bag is ingested.
Agent Prompt
## Issue description
`serializeActivityAttributes()` enforces a size cap by truncating the *serialized JSON string* and appending `…`, which can make the stored value invalid JSON.
## Issue Context
`attributes_json` is treated as JSON elsewhere (e.g. tests parse it), so the serializer must return either **valid JSON** or **null**.
## Fix Focus Areas
- src/shared/activity-attributes.ts[63-73]
- src/shared/__tests__/activity-attributes.test.ts[1-63]
## Suggested fix
- Change `serializeActivityAttributes()` to guarantee valid JSON:
- Option A (simplest): if JSON exceeds the cap, return `null`.
- Option B (preferred): truncate at the **attribute/value** level while keeping JSON valid (e.g., drop lowest-priority keys, or truncate long string values, then `JSON.stringify()` again until under the cap).
- Add a regression test that passes an attributes object whose serialized size exceeds `ACTIVITY_ATTRIBUTES_MAX_JSON_CHARS` and asserts the result is either `null` or `JSON.parse(result)` succeeds.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| ): SkillContentResult | null { | ||
| if (skill.type !== "plugin" && !skill.sourcePlugin) return null; | ||
| const pluginId = skill.sourcePlugin?.id; | ||
| if ( |
There was a problem hiding this comment.
2. Plugin fallback guard wrong 🐞 Bug ≡ Correctness
resolvePluginSkillContent() uses skill.type !== "plugin" && !skill.sourcePlugin as its early-return guard, so non-plugin skill types that still carry sourcePlugin metadata can flow into the unpacked-plugin resolution path. With inconsistent/stale capability data, resolveSkillContent() can return SKILL.md content from the unpacked plugin tree instead of the skill's declared source.
Agent Prompt
## Issue description
`resolvePluginSkillContent()` is intended as a pre-install fallback for **plugin** skills, but its guard condition allows the code path to run for non-plugin skills whenever `sourcePlugin` is present.
## Issue Context
`Skill.sourcePlugin` is optional on the `Skill` interface for any skill type; relying on it alone can accidentally classify a non-plugin skill as plugin-backed.
## Fix Focus Areas
- src/server/skill-content.ts[230-242]
- src/types/capabilities.ts[284-289]
## Suggested fix
- Tighten the guard to only allow plugin skills, e.g.:
- `if (skill.type !== "plugin" || !skill.sourcePlugin) return null;`
- (Optional) Add a unit test that constructs a non-`plugin` Skill with `sourcePlugin` set and asserts `resolveSkillContent()` does **not** resolve via the unpacked plugin tree.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const run: ActivityRun = { | ||
| id: generationId || first.id, | ||
| conversationId, |
There was a problem hiding this comment.
3. Generation id not namespaced 🐞 Bug ≡ Correctness
For correlated activity, ActivityRun.id is set to generationId, but ActivityFeed selects the run via runs.find(r => r.id === selectedId) across all conversations. If two conversations reuse the same generation_id value, the dialog can open the wrong run because the id is not namespaced by conversation.
Agent Prompt
## Issue description
Correlated runs use `generation_id` as the run's identifier, but the UI selects a run by `id` alone across the flattened run list. Duplicate generation ids across conversations make selection ambiguous.
## Issue Context
The UI flattens all conversations into a single `runs` array and does a first-match lookup by id.
## Fix Focus Areas
- web-ui/src/features/projects/components/activity/groupActivityRuns.ts[194-218]
- web-ui/src/features/projects/components/activity/ActivityFeed.tsx[149-163]
- web-ui/src/features/projects/components/activity/groupActivityRuns.test.ts[98-152]
## Suggested fix
- Make `ActivityRun.id` globally unique by namespacing, e.g. `id: `${conversationId}:${generationId}``.
- Keep `conversationId`/`generationId` fields as-is for display/correlation.
- Update `ActivityFeed` selection to use the new composite id.
- Add a test with two different `conversation_id` values sharing the same `generation_id` and assert selecting one opens the correct run.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Pull request overview
This release PR extends capa’s observability and reliability across agent activity ingestion, plugin resolution/installation (including Cursor marketplace monorepos), and wrap session startup—plus corresponding server/UI updates to persist and present correlated activity (conversation → generation → spans).
Changes:
- Add provider-configurable activity correlation/attributes/result extraction, persist those fields on
tool_calls, and update the web UI to group activity by conversation/generation. - Improve plugin robustness: isolate per-plugin failures, support monorepo
gitPathinstalls + bounded nested-plugin lookup by entry id, and allow SKILL.md content to be served pre-install from unpacked plugin trees. - Improve wrap/activity ingest reliability: always emit valid gate JSON on stdout, strip BOM on Windows hook stdin, ensure wrap warm-starts have a running server, and fix late SSE subscriber “reconnecting” state.
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web-ui/src/types/api.ts | Extends ToolCallRecord API typing with correlation/attribute fields. |
| web-ui/src/features/projects/project-events.ts | Adds test reset helper and late-subscriber EventSource.OPEN notification. |
| web-ui/src/features/projects/components/activity/groupActivityRuns.ts | Introduces conversation/generation grouping and labels for activity feed. |
| web-ui/src/features/projects/components/activity/groupActivityRuns.test.ts | Adds coverage for conversation/generation grouping behavior. |
| web-ui/src/features/projects/components/activity/ActivityFeed.tsx | Updates UI to render conversation headers and nested generations. |
| web-ui/src/features/projects/tests/project-events.test.ts | Adds tests for late-subscriber open notifications and reset helper. |
| src/types/providers.ts | Adds provider hook integration types for correlation/attributes/result fields. |
| src/types/database.ts | Extends DB ToolCallRecord shape with new persisted fields. |
| src/shared/providers/entries/cursor.ts | Declares Cursor correlation/attributes/result field mappings. |
| src/shared/providers/entries/codex.ts | Declares Codex correlation/attributes/result field mappings (Claude-like). |
| src/shared/providers/entries/claude-code.ts | Declares Claude Code correlation/attributes/result field mappings. |
| src/shared/plugin-manifest/index.ts | Re-exports new nested-plugin resolver helper. |
| src/shared/plugin-manifest/detect.ts | Adds bounded nested-plugin lookup by entry id for monorepos. |
| src/shared/lockfile.ts | Adds ability to remove a plugin pin after partial resolve failure. |
| src/shared/agent-activity.ts | Adjusts system activity events to omit all before* hooks. |
| src/shared/agent-activity-normalize.ts | Normalizes correlation/attributes/result preview and updated skip logic. |
| src/shared/activity-result.ts | Implements provider-configured result extraction from hook stdin. |
| src/shared/activity-correlation.ts | Implements provider-configured conversation/generation id extraction. |
| src/shared/activity-attributes.ts | Implements provider-configured attribute extraction and serialization. |
| src/shared/tests/agent-activity.test.ts | Updates tests for new normalization behavior and hook selection. |
| src/shared/tests/activity-result.test.ts | Adds tests for provider-configured result extraction. |
| src/shared/tests/activity-correlation.test.ts | Adds tests for provider-configured correlation extraction. |
| src/shared/tests/activity-attributes.test.ts | Adds tests for provider-configured attribute extraction/serialization. |
| src/server/tool-call-tracer.ts | Persists correlation/attributes and optionally inherits correlation for MCP/shell traces. |
| src/server/skill-content.ts | Adds pre-install plugin skill content resolution from unpacked plugin trees. |
| src/server/resolve-effective-capabilities.ts | Documents per-plugin failure isolation behavior. |
| src/server/project-routes.ts | Threads projectId into skill description resolution for plugin unpack fallback. |
| src/server/mcp-meta-routes.ts | Threads projectId into skill content resolution for plugin unpack fallback. |
| src/server/activity-routes.ts | Accepts/persists correlation/model/attributes and disables correlation inheritance for hook ingest. |
| src/server/tests/tool-call-tracer.test.ts | Adds tests for correlation inheritance behavior and explicit override behavior. |
| src/server/tests/skill-content-plugin.test.ts | Adds tests for plugin unpack skill content/description and precedence rules. |
| src/server/tests/activity-routes.test.ts | Adds tests for persisting correlation/model/attributes and no cross-provider inheritance. |
| src/db/tool-calls.ts | Extends inserts and adds correlation lookup + generation-aware page expansion. |
| src/db/schema.ts | Adds new columns and project+conversation index. |
| src/db/database.ts | Exposes latest-activity correlation lookup through DB facade. |
| src/cli/commands/wrap.ts | Ensures server is running for warm wrap sessions. |
| src/cli/commands/wrap-ensure-server.ts | Adds helper to start/verify server for wrap. |
| src/cli/commands/plugin-install.ts | Isolates per-plugin failures, adds monorepo id fallback, and rolls back partial plugin state. |
| src/cli/commands/activity-ingest.ts | Ensures gate stdout JSON, strips BOM, uses client-connectable origin, posts correlation/attributes. |
| src/cli/commands/tests/wrap-ensure-server.test.ts | Adds tests for wrap server ensure helper. |
| src/cli/commands/tests/plugin-resolve-isolate.test.ts | Adds tests ensuring one bad plugin doesn’t wipe others and partial rollback works. |
| src/cli/commands/tests/plugin-monorepo-id-fallback.test.ts | Adds tests for nested plugin resolution by entry id. |
| src/cli/commands/tests/activity-ingest.test.ts | Adds tests for gate stdout behavior, BOM stripping, and origin building. |
| registries/cursor-marketplace/adapter.ts | Supports gitPath/gitRef and emits ::subpath installs with traversal rejection. |
| registries/cursor-marketplace/adapter.test.ts | Adds tests for gitPath/gitRef parsing and traversal rejection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function serializeActivityAttributes( | ||
| attributes: ActivityAttributes, | ||
| ): string | null { | ||
| if (!attributes || Object.keys(attributes).length === 0) return null; | ||
| try { | ||
| const json = JSON.stringify(attributes); | ||
| if (json.length <= ACTIVITY_ATTRIBUTES_MAX_JSON_CHARS) return json; | ||
| return `${json.slice(0, ACTIVITY_ATTRIBUTES_MAX_JSON_CHARS - 1)}…`; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
| expect(result).not.toBeNull(); | ||
| expect(result!.content).toContain("Cmd body."); | ||
| }); | ||
| it("prefers installed provider copy over unpacked plugin tree after install", () => { |
| const pluginRoot = join(repoRoot, cleaned); | ||
| if (!existsSync(pluginRoot)) return null; | ||
| const manifest = detectAndParseManifest(pluginRoot, preferredProviders); | ||
| if (!manifest) return null; | ||
| const dirName = cleaned.split("/").filter(Boolean).pop() ?? cleaned; | ||
| return { | ||
| entry: { | ||
| subpath: cleaned, | ||
| manifestName: manifest.name || dirName, | ||
| dirName, | ||
| manifestFile: "", | ||
| }, | ||
| manifest, | ||
| }; |
|
Deferring comment fixes to next release. |
Keep attributes_json valid JSON under the size cap, tighten plugin skill fallback guards, namespace activity run ids by conversation, populate nested plugin manifest paths, and skip Cursor id reconcile when generation chat ids conflict.
Summary
This release merges 24 commits on
develop(including PRs #163–#167) with 45 files changed (~2,743 additions, ~159 deletions). The work falls into three areas: agent activity observability, plugin install and skill content, and wrap / activity-ingest reliability (especially Cursor gate hooks and Windows).Agent activity (feed, ingest, and persistence)
hooks.activityCorrelationfield maps (no hardcoded provider IDs). Ingest persistsconversation_id,generation_id,model, andattributes_jsonontool_calls, with a new index for project + conversation queries.hooks.activityAttributesextracts envelope fields (model, versions, etc.) into bounded JSON for storage and future telemetry.tool_response, Cursortool_output) so Claude Code spans show output again.capa shtraces inherit the active conversation/generation;afterShellrows remain so Bash/Shell calls appear in the feed.beforeFileReadfrom agent activity (Cursor already records reads via post-tool hooks; duplicate rows and gate risk removed).Plugin system
gitPath/gitRefresolve toowner/repo::subpathinstalls; BOM stripping and bounded monorepo id lookup from review feedback.SKILL.mdcan be served from the unpacked plugin tree under~/.capa/pluginsso the UI shows content as soon as a plugin is added; post-install provider copies are preferred when present, with mtime-cached directory scans.Wrap, activity-ingest, and live indicator (#166, #167)
activity-ingestalways emits valid allow/continue JSON in afinallyblock so ingest stays fail-open and does not block gate events with empty stdout.wrap-ensure-server/ warm reuse starts the server when needed; client-connectable origin for wildcard/IPv6 server hosts; test mock changes to avoid CI pollution in unrelated wrap tests.EventSourceis already open so the activity feed does not stay stuck on “reconnecting” after capabilities sync connects first.Database / API notes
tool_calls:conversation_id,generation_id,model,attributes_json, plus indexidx_tool_calls_project_conversation. Existing databases pick these up viaensureColumn.activity-routes,ActivityFeed, andgroupActivityRuns.Test plan
tool_callscolumns populate during a Cursor or Claude Code session.gitPathand confirm manifest/skill resolution under the subpath.beforeFileRead/ tool gates) still return allow JSON; activity ingest does not block the agent.capa wrap: Cold and warm start both reach a running server; activity ingest reaches the server with expected origin on non-localhost bind addresses.Included pull requests
gitPath/ entry idcapa install