Skip to content

feat: skip/filter subagent observations (Dynamic Workflows workflow-subagent) — #2736#2741

Open
NRKZOZW wants to merge 1 commit into
thedotmack:mainfrom
NRKZOZW:feat/skip-subagent-observations-2736
Open

feat: skip/filter subagent observations (Dynamic Workflows workflow-subagent) — #2736#2741
NRKZOZW wants to merge 1 commit into
thedotmack:mainfrom
NRKZOZW:feat/skip-subagent-observations-2736

Conversation

@NRKZOZW

@NRKZOZW NRKZOZW commented Jun 1, 2026

Copy link
Copy Markdown

Closes #2736.

Problem

Claude Code Dynamic Workflows fan a single prompt out to tens-to-hundreds of parallel subagents (agent_type: workflow-subagent). Every tool call by every subagent fires a PostToolUse hook → claude-mem creates one provider-analyzed observation per call, so a single run emits hundreds-to-thousands of low-signal observations.

As the issue documents with real DB evidence (634 / 699 ≈ 91% of a day's observations came from workflow-subagent), this:

  1. Exhausts the provider's free-tier quota (HTTP 429), after which the restart guard trips (session is dead, clearing pending and terminating) and the rest of the run's observations — including valuable main-session work — are dropped.
  2. Tanks signal-to-noise — ephemeral verification/explore subagent steps are rarely worth persisting cross-session.

Subagent summaries are already skipped; subagent observations were not. The only existing lever (CLAUDE_MEM_SKIP_TOOLS) is global by tool name and can't target subagents without also dropping main-session observations.

Solution

Two settings, filtered before any worker HTTP round-trip or provider request — so the request and the provider tokens are saved:

Setting Default Behavior
CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS false When true, skip every observation carrying an agentId (any subagent). The robust, type-independent lever.
CLAUDE_MEM_SKIP_AGENT_TYPES (empty) Comma-separated agent_type values to skip, e.g. workflow-subagent,Explore. Surgical control.

The two combine as a union (skip if the global toggle matches or the agent_type is listed) — simpler than, and a strict superset of, the issue's "priority" framing, and it satisfies every acceptance-criteria bullet.

Design decisions (beyond the issue text)

  • Defaults preserve current behavior, honoring the issue's AC checkbox "defaults preserve current behavior." Rather than silently dropping data in the shipped default, workflow-subagent is documented as the recommended value. Flipping the default to skip workflow-subagent out of the box is a one-line change in SettingsDefaultsManager if you'd prefer that — I went conservative deliberately. (Note: a global agentId toggle is more robust than a type-string match, since a non-workflow-subagent burst trips the same restart guard.)
  • Two filtering points, one source of truth. A new pure helper src/shared/should-skip-agent-observation.ts is called from:
    • the hook handler (src/cli/handlers/observation.ts) ahead of the runtime branch, so it covers both the worker and server-beta runtimes and avoids the round-trip entirely; and
    • the worker ingest path (src/services/worker/http/shared.ts ingestObservation) as defense-in-depth for any non-hook caller, before queueObservation (the provider call).
      This mirrors the existing dual-check pattern for CLAUDE_MEM_EXCLUDED_PROJECTS.

Acceptance criteria

  • CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS=true drops every observation carrying an agentId before any provider call.
  • CLAUDE_MEM_SKIP_AGENT_TYPES=workflow-subagent,Explore drops only those types.
  • Main-session observations are unaffected.
  • Documented in the settings reference; defaults preserve current behavior.

Tests & verification

  • New unit tests for the helper (tests/shared/should-skip-agent-observation.test.ts): default no-skip, global toggle, per-type list, agentType-without-agentId, whitespace/dedupe parsing, union precedence.
  • New hook-handler integration tests (tests/cli/handlers/observation-subagent-skip.test.ts): asserts the worker HTTP call / recordEvent is not made when skipped (would fail if the filter were removed), across both the worker and server-beta runtimes, and that main-session + non-listed types still flow.
  • Locally ran the equivalent of CI (ci.yml): npm run typecheck clean · npm run build succeeds (bundle-size guardrails pass) · full bun test 2080 pass / 0 fail. (Actions on a fork PR waits for a maintainer to approve the run.)
  • Rebuilt plugin bundles are included (per repo convention).

Out of scope / future work

An optional per-session/minute throughput guard (the issue's secondary ask) would defend against runaway bursts from any source, not just known agent types. It needs worker-side rate state and risks non-deterministic drops, so I left it out to keep this change deterministic and focused; happy to follow up.

🤖 Generated with Claude Code

…ack#2736

Claude Code Dynamic Workflows fan a single prompt out to tens-to-hundreds of
parallel subagents (agent_type `workflow-subagent`). Every tool call fires a
PostToolUse hook → claude-mem creates one provider-analyzed observation per
call, so a single run emits hundreds-to-thousands of low-signal observations.
That exhausts the provider's free-tier quota (HTTP 429) and trips the restart
guard, dropping the rest of the run — including valuable main-session work.

Add two settings, filtered BEFORE any worker HTTP call or provider request so
the round-trip and the provider tokens are saved:

- CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS (default `false`) — skip every
  observation carrying an agentId. The robust, type-independent lever.
- CLAUDE_MEM_SKIP_AGENT_TYPES (default empty) — comma-separated agent_type skip
  list, e.g. `workflow-subagent,Explore`.

Both default off to preserve current behavior (per the issue's acceptance
criteria); `workflow-subagent` is documented as the recommended value. The two
combine as a union: skip if the global toggle matches OR the agent_type is listed.

Implementation:
- New pure helper src/shared/should-skip-agent-observation.ts — single source of
  truth for both filtering points.
- Hook handler (src/cli/handlers/observation.ts) filters ahead of the runtime
  branch, so it covers both the worker and server-beta runtimes.
- Worker ingestObservation (src/services/worker/http/shared.ts) filters again as
  defense-in-depth for any non-hook caller, before the provider call.
- Settings wired into SettingsDefaultsManager (interface + defaults) and the
  viewer constants; documented in docs/public/configuration.mdx.
- Tests: helper unit tests + hook-handler integration covering worker and
  server-beta runtimes, main-session-unaffected, global toggle, and per-type list.
- Rebuilt plugin bundles.

Future work (out of scope): an optional per-session/minute throughput guard for
runaway bursts from any source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two new opt-in environment settings (CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS and CLAUDE_MEM_SKIP_AGENT_TYPES) to filter out subagent observations before any provider round-trip, preventing Dynamic Workflows fan-out from exhausting provider quota and degrading signal-to-noise. The implementation is well-structured: a single pure helper is called at both the hook-handler level and as defense-in-depth inside the worker ingest path.

  • A new pure helper should-skip-agent-observation.ts encodes the skip decision; it is called ahead of the runtime branch in observation.ts, protecting both the worker and server-beta paths, and is called again in ingestObservation for non-hook callers.
  • Both settings default to off, preserving existing behavior; the global toggle (SKIP_SUBAGENT_OBSERVATIONS) keys on agentId (safe for main-session), while the per-type list (SKIP_AGENT_TYPES) keys only on agentType—no agentId required—creating an asymmetry that can drop main-session observations if they ever carry a matching agentType.

Confidence Score: 4/5

Safe to merge with the per-type filter asymmetry understood; the global toggle is the recommended lever and is correctly bounded to agentId.

The global toggle is correctly gated on agentId and cannot touch main-session observations. The per-type list matches on agentType alone with no agentId guard, so a main-session hook payload carrying a matching agentType would be silently dropped—contradicting the documented guarantee. Whether this causes real data loss depends on whether Claude Code ever emits agentType on main-session payloads.

src/shared/should-skip-agent-observation.ts — the per-type list branch (lines 55-64) and the test at tests/shared/should-skip-agent-observation.test.ts line 71 that validates agentId-absent skipping.

Important Files Changed

Filename Overview
src/shared/should-skip-agent-observation.ts New pure helper implementing the skip-decision logic. Correct for the global toggle (gates on agentId), but the per-type list check gates on agentType alone—no agentId required—creating an asymmetry that can silently drop main-session observations carrying a matching agentType.
src/cli/handlers/observation.ts Skip check inserted correctly ahead of the runtime branch; covers both worker and server-beta paths. Clean integration with loadFromFileOnce().
src/services/worker/http/shared.ts Defense-in-depth call to shouldSkipAgentObservation before queueObservation is correct; uses a non-null assertion (reason!) that is technically safe but fragile.
src/shared/SettingsDefaultsManager.ts Two new settings added with safe defaults (false / empty); properly typed in SettingsDefaults interface.
tests/shared/should-skip-agent-observation.test.ts Comprehensive unit tests covering defaults, global toggle, per-type list, union semantics, and whitespace/dedup parsing.
tests/cli/handlers/observation-subagent-skip.test.ts Integration tests cover both worker and server-beta runtimes, with proper mock cleanup in afterAll to prevent leak to later test files.
docs/public/configuration.mdx New section documents both settings well; the claim 'Main-session observations are unaffected' is only strictly true for the global toggle—the per-type list can match agentType without an agentId.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[PostToolUse hook fires] --> B{shouldTrackProject?}
    B -- No --> Z[return: skip]
    B -- Yes --> C{shouldSkipAgentObservation?}
    C -- "SKIP_SUBAGENT=true AND agentId present" --> D[reason: subagent_observation]
    C -- "agentType in SKIP_AGENT_TYPES, no agentId needed" --> E[reason: agent_type_excluded]
    D --> Z
    E --> Z
    C -- No skip --> F{resolveRuntimeContext}
    F -- server-beta --> G[client.recordEvent]
    G -- success --> Z
    G -- fallback --> H[dispatchToWorker]
    F -- worker --> H
    H --> I[Worker HTTP POST]
    I --> J{ingestObservation}
    J --> K{project excluded?}
    K -- Yes --> Z
    K -- No --> L{tool excluded?}
    L -- Yes --> Z
    L -- No --> M{shouldSkipAgentObservation defense-in-depth}
    M -- Skip --> Z
    M -- No --> N[queueObservation to provider]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/shared/should-skip-agent-observation.ts:55-64
**Per-type list can silently drop main-session observations**

The global toggle (`CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS`) correctly gates on `agentId`, so it is provably safe for main-session events. The per-type list path (`CLAUDE_MEM_SKIP_AGENT_TYPES`) checks only `agentType` — no `agentId` is required — so any main-session observation that carries a matching `agentType` (without an `agentId`) is dropped. The test at line 71 of the test file explicitly validates this: `shouldSkipAgentObservation(undefined, 'workflow-subagent', s).skip === true`. This contradicts the acceptance criterion "Main-session observations are unaffected" and the docs' claim under `CLAUDE_MEM_SKIP_AGENT_TYPES`. If Claude Code ever emits `agentType` on a main-session hook payload (even without `agentId`), those observations would be silently lost.

### Issue 2 of 2
src/services/worker/http/shared.ts:123-125
When `skip` is `true` the `reason` field is always populated, but the `AgentSkipDecision` type declares it as optional (`reason?: AgentSkipReason`). The non-null assertion silences TypeScript here but will throw at runtime if the invariant is ever broken by a future change to `shouldSkipAgentObservation`. Replacing with `?? 'agent_skip'` makes the fallback explicit.

```suggestion
  if (agentSkip.skip) {
    return { ok: true, status: 'skipped', reason: agentSkip.reason ?? 'agent_skip' };
  }
```

Reviews (1): Last reviewed commit: "feat: skip/filter subagent observations ..." | Re-trigger Greptile

Comment on lines +55 to +64
* Defaults preserve current behavior: with the global toggle off and an empty
* skip list, this never skips.
*/
export function shouldSkipAgentObservation(
agentId: string | undefined | null,
agentType: string | undefined | null,
settings: AgentSkipSettings,
): AgentSkipDecision {
if (settings.CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS === 'true' && agentId) {
return { skip: true, reason: 'subagent_observation' };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Per-type list can silently drop main-session observations

The global toggle (CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS) correctly gates on agentId, so it is provably safe for main-session events. The per-type list path (CLAUDE_MEM_SKIP_AGENT_TYPES) checks only agentType — no agentId is required — so any main-session observation that carries a matching agentType (without an agentId) is dropped. The test at line 71 of the test file explicitly validates this: shouldSkipAgentObservation(undefined, 'workflow-subagent', s).skip === true. This contradicts the acceptance criterion "Main-session observations are unaffected" and the docs' claim under CLAUDE_MEM_SKIP_AGENT_TYPES. If Claude Code ever emits agentType on a main-session hook payload (even without agentId), those observations would be silently lost.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/shared/should-skip-agent-observation.ts
Line: 55-64

Comment:
**Per-type list can silently drop main-session observations**

The global toggle (`CLAUDE_MEM_SKIP_SUBAGENT_OBSERVATIONS`) correctly gates on `agentId`, so it is provably safe for main-session events. The per-type list path (`CLAUDE_MEM_SKIP_AGENT_TYPES`) checks only `agentType` — no `agentId` is required — so any main-session observation that carries a matching `agentType` (without an `agentId`) is dropped. The test at line 71 of the test file explicitly validates this: `shouldSkipAgentObservation(undefined, 'workflow-subagent', s).skip === true`. This contradicts the acceptance criterion "Main-session observations are unaffected" and the docs' claim under `CLAUDE_MEM_SKIP_AGENT_TYPES`. If Claude Code ever emits `agentType` on a main-session hook payload (even without `agentId`), those observations would be silently lost.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +123 to +125
if (agentSkip.skip) {
return { ok: true, status: 'skipped', reason: agentSkip.reason! };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 When skip is true the reason field is always populated, but the AgentSkipDecision type declares it as optional (reason?: AgentSkipReason). The non-null assertion silences TypeScript here but will throw at runtime if the invariant is ever broken by a future change to shouldSkipAgentObservation. Replacing with ?? 'agent_skip' makes the fallback explicit.

Suggested change
if (agentSkip.skip) {
return { ok: true, status: 'skipped', reason: agentSkip.reason! };
}
if (agentSkip.skip) {
return { ok: true, status: 'skipped', reason: agentSkip.reason ?? 'agent_skip' };
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/worker/http/shared.ts
Line: 123-125

Comment:
When `skip` is `true` the `reason` field is always populated, but the `AgentSkipDecision` type declares it as optional (`reason?: AgentSkipReason`). The non-null assertion silences TypeScript here but will throw at runtime if the invariant is ever broken by a future change to `shouldSkipAgentObservation`. Replacing with `?? 'agent_skip'` makes the fallback explicit.

```suggestion
  if (agentSkip.skip) {
    return { ok: true, status: 'skipped', reason: agentSkip.reason ?? 'agent_skip' };
  }
```

How can I resolve this? If you propose a fix, please make it concise.

IgorTavcar added a commit to IgorTavcar/claude-mem that referenced this pull request Jun 1, 2026
…-observations-2736

feat: skip/filter subagent observations (Dynamic Workflows `workflow-subagent`) — thedotmack#2736

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
IgorTavcar added a commit to IgorTavcar/claude-mem that referenced this pull request Jun 1, 2026
…tee main-session observations are never dropped

Greptile flagged that CLAUDE_MEM_SKIP_AGENT_TYPES matched on agentType
alone, with no agentId guard, so a main-session payload carrying a listed
agent_type (without an agentId) would be silently dropped — contradicting
the PR's own acceptance criterion "Main-session observations are unaffected."

Both skip levers now require an agentId first: a payload without one is
treated as main-session and never skipped. Real subagents always carry both
an agentId and an agentType, so this doesn't weaken the per-type lever in
practice — it just makes the guarantee true by construction.

- src/shared/should-skip-agent-observation.ts: early-return NO_SKIP when no
  agentId; both branches now reachable only for genuine subagents.
- tests: per-type test now asserts a listed agentType without an agentId is
  NOT skipped (was asserting the opposite).
- docs/configuration.mdx: clarify both levers only apply to agentId-carrying
  observations.
- Rebuilt plugin bundles (worker-service.cjs, server-beta-service.cjs).

typecheck clean · full bun test 2080 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NRKZOZW

NRKZOZW commented Jun 5, 2026

Copy link
Copy Markdown
Author

Context: #2736 was consolidated into the plan-12 roadmap (#2785) under Ingestion & filtering. This PR is the concrete, tested implementation of that slice — I've flagged it on #2785 so it can serve as the base rather than a from-scratch reimplementation.

Happy to rebase or reshape to match the plan-12 slicing, and to address review feedback — including the per-type-vs-main-session semantics the automated review flagged (the per-type list matches on agent_type alone by design; I can tighten it or adjust the docs' wording, whichever you prefer) and the explicit reason fallback.

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.

feat: skip/throttle subagent observations (esp. Dynamic Workflows workflow-subagent) — re-raise of #2303

1 participant