Skip to content

Commit 52f4a66

Browse files
amascia-ggclaude
andcommitted
feat(ai): surface server-dropped agent-activity records
The server reports how many records it could not scan and dropped (never stored). Thread that count through AgentActivityBatchResult and the report, and print it in the `ai discover --history` summary when non-zero, so drops are visible instead of silently making the counts not add up. Issue: NHI-1628 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 985da1c commit 52f4a66

3 files changed

Lines changed: 27 additions & 14 deletions

File tree

ggshield/cmd/ai/discover.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def _summarize_discovery(
148148
"ingested": activity_report.ingested,
149149
"duplicates": activity_report.duplicates,
150150
"failed_batches": activity_report.failed_batches,
151+
"dropped": activity_report.dropped,
151152
}
152153
return summary
153154

@@ -223,3 +224,6 @@ def print_summary(summary: Dict[str, Any]) -> None:
223224
if agent_activity.get("failed_batches", 0) > 0:
224225
label = format_text("Failed batches:", STYLE["detector_line_start"])
225226
click.echo(f" • {label} {agent_activity['failed_batches']:,}")
227+
if agent_activity.get("dropped", 0) > 0:
228+
label = format_text("Dropped (unscannable):", STYLE["detector_line_start"])
229+
click.echo(f" • {label} {agent_activity['dropped']:,}")

ggshield/verticals/ai/agent_activity/orchestrator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class AgentActivityBatchResult:
2929
ingested: int
3030
duplicates: int
3131
success: bool
32+
dropped: int = 0
3233

3334

3435
@dataclass
@@ -37,14 +38,14 @@ class AgentActivityReport:
3738
ingested: int = 0
3839
duplicates: int = 0
3940
failed_batches: int = 0
41+
# Records the server could not scan and dropped (never stored).
42+
dropped: int = 0
4043

4144

4245
def send_agent_activity_batch(
4346
client: GGClient, events: List[AgentActivityEvent], user: UserInfo
4447
) -> AgentActivityBatchResult:
4548
"""Serialise events and submit them as one batch (with the reporting user)."""
46-
if not events:
47-
return AgentActivityBatchResult(ingested=0, duplicates=0, success=True)
4849
payload = [e.to_dict() for e in events]
4950
response = client.send_agent_activity(payload, user=user.to_dict())
5051
if isinstance(response, Detail):
@@ -54,6 +55,7 @@ def send_agent_activity_batch(
5455
ingested=response.ingested,
5556
duplicates=response.duplicates,
5657
success=True,
58+
dropped=getattr(response, "dropped", 0),
5759
)
5860

5961

@@ -83,6 +85,7 @@ def flush() -> None:
8385
else:
8486
report.ingested += result.ingested
8587
report.duplicates += result.duplicates
88+
report.dropped += result.dropped
8689
if not result.success:
8790
report.failed_batches += 1
8891
buffer.clear()

tests/unit/verticals/ai/agent_activity/test_orchestrator.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
def test_send_agent_activity_batch_serialises_and_calls_client() -> None:
1919
client = MagicMock()
2020
client.send_agent_activity.return_value = MagicMock(
21-
success=True, ingested=2, duplicates=0
21+
success=True, ingested=2, duplicates=0, dropped=0
2222
)
2323
events = [
2424
AgentActivityEvent(
@@ -59,13 +59,6 @@ def test_send_agent_activity_batch_serialises_and_calls_client() -> None:
5959
assert result.ingested == 2
6060

6161

62-
def test_send_agent_activity_batch_empty_list_short_circuits() -> None:
63-
client = MagicMock()
64-
result = send_agent_activity_batch(client, [], _USER)
65-
client.send_agent_activity.assert_not_called()
66-
assert result.ingested == 0
67-
68-
6962
def test_send_agent_activity_batch_treats_detail_as_failure() -> None:
7063
"""An API error (Detail) is reported as a failed batch, not an ingest."""
7164
client = MagicMock()
@@ -105,7 +98,7 @@ def _event(agent_name: str, i: int) -> AgentActivityEvent:
10598
def test_collect_agent_activity_attaches_user_to_each_batch() -> None:
10699
client = MagicMock()
107100
client.send_agent_activity.return_value = MagicMock(
108-
success=True, ingested=1, duplicates=0
101+
success=True, ingested=1, duplicates=0, dropped=0
109102
)
110103
collect_agent_activity(client, _USER, [_make_agent("a", [_event("a", 1)])])
111104

@@ -120,7 +113,7 @@ def test_collect_agent_activity_batches_in_chunks_of_batch_size() -> None:
120113
agent = _make_agent("a", events)
121114
client = MagicMock()
122115
client.send_agent_activity.return_value = MagicMock(
123-
success=True, ingested=BATCH_SIZE, duplicates=0
116+
success=True, ingested=BATCH_SIZE, duplicates=0, dropped=0
124117
)
125118

126119
report = collect_agent_activity(client, _USER, [agent])
@@ -134,7 +127,7 @@ def test_collect_agent_activity_aggregates_per_agent_counts() -> None:
134127
events_b = [_event("b", 2)]
135128
client = MagicMock()
136129
client.send_agent_activity.return_value = MagicMock(
137-
success=True, ingested=2, duplicates=0
130+
success=True, ingested=2, duplicates=0, dropped=0
138131
)
139132

140133
report = collect_agent_activity(
@@ -166,7 +159,9 @@ def test_collect_agent_activity_flushes_on_byte_threshold() -> None:
166159
for i in range(3)
167160
]
168161
client = MagicMock()
169-
client.send_agent_activity.return_value = MagicMock(ingested=1, duplicates=0)
162+
client.send_agent_activity.return_value = MagicMock(
163+
ingested=1, duplicates=0, dropped=0
164+
)
170165

171166
report = collect_agent_activity(client, _USER, [_make_agent("a", events)])
172167

@@ -184,3 +179,14 @@ def test_collect_agent_activity_counts_network_error_as_failed_batch() -> None:
184179
assert report.parsed == 1
185180
assert report.ingested == 0
186181
assert report.failed_batches == 1
182+
183+
184+
def test_collect_agent_activity_aggregates_dropped_from_response() -> None:
185+
"""The server's dropped count (records it could not scan) is surfaced."""
186+
client = MagicMock()
187+
client.send_agent_activity.return_value = MagicMock(
188+
success=True, ingested=0, duplicates=0, dropped=3
189+
)
190+
report = collect_agent_activity(client, _USER, [_make_agent("a", [_event("a", 1)])])
191+
192+
assert report.dropped == 3

0 commit comments

Comments
 (0)