Skip to content

Commit c6c6551

Browse files
authored
T5-P6-A10-WP1 Unknown-NDJSON-Vocabulary Counters (#4120)
## Summary Add `ndjson_unknown_event_count` and `ndjson_unknown_item_count` integer counters to the Codex NDJSON parsing pipeline. The counters originate at two parser layers (`_CodexParseAccumulator` for batch parsing, `CodexStreamParser` for streaming), flow through `AgentSessionResult.raw`, and land on `ClaudeSessionResult` via `_adapt_agent_result`. This makes unknown-vocabulary occurrences machine-readable for downstream WPs (P6-A10-WP2+) without log scraping. ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260626-235325-737964/.autoskillit/temp/make-plan/t5_p6_a10_wp1_unknown_ndjson_counters_plan_2026-06-26_235500.md` Closes #4045 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 59 | 16.6k | 892.4k | 98.0k | 43 | 84.0k | 14m 40s | | verify* | sonnet | 1 | 92 | 9.9k | 517.6k | 59.6k | 29 | 41.1k | 4m 21s | | implement* | MiniMax-M3 | 1 | 97.8k | 11.5k | 2.5M | 0 | 97 | 0 | 4m 50s | | audit_impl* | sonnet | 1 | 44 | 13.6k | 165.6k | 44.3k | 13 | 42.1k | 7m 33s | | prepare_pr* | MiniMax-M3 | 1 | 42.6k | 1.9k | 112.4k | 43.9k | 11 | 0 | 37s | | compose_pr* | MiniMax-M3 | 1 | 36.0k | 2.1k | 142.6k | 0 | 11 | 0 | 35s | | **Total** | | | 176.6k | 55.6k | 4.3M | 98.0k | | 167.1k | 32m 37s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 129 | 19020.4 | 0.0 | 89.1 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **129** | 33211.0 | 1295.6 | 431.2 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 59 | 16.6k | 892.4k | 84.0k | 14m 40s | | sonnet | 2 | 136 | 23.5k | 683.2k | 83.1k | 11m 54s | | MiniMax-M3 | 3 | 176.4k | 15.5k | 2.7M | 0 | 6m 2s |
1 parent 6c51ed2 commit c6c6551

6 files changed

Lines changed: 115 additions & 0 deletions

File tree

src/autoskillit/execution/backends/_codex_parse.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class _CodexParseAccumulator:
3737
success: bool = False
3838
error_message: str = ""
3939
error_code: str = ""
40+
ndjson_unknown_event_count: int = 0
41+
ndjson_unknown_item_count: int = 0
4042

4143

4244
def _scan_codex_ndjson(stdout: str) -> _CodexParseAccumulator:
@@ -56,6 +58,7 @@ def _scan_codex_ndjson(stdout: str) -> _CodexParseAccumulator:
5658
event_type = CodexEventType.from_ndjson(obj.get("type", ""))
5759
if event_type == CodexEventType.UNKNOWN:
5860
logger.warning("codex_ndjson_unknown_event_type", type=obj.get("type", ""))
61+
acc.ndjson_unknown_event_count += 1
5962
continue
6063
if event_type == CodexEventType.THREAD_STARTED:
6164
acc.session_id = obj.get("thread_id", "")
@@ -103,6 +106,7 @@ def _scan_codex_ndjson(stdout: str) -> _CodexParseAccumulator:
103106
continue
104107
elif item_type == CodexItemType.UNKNOWN:
105108
logger.warning("codex_ndjson_unknown_item_type", item_type=item.get("type", ""))
109+
acc.ndjson_unknown_item_count += 1
106110
continue
107111
elif event_type == CodexEventType.TURN_COMPLETED:
108112
usage = obj.get("usage")
@@ -186,6 +190,8 @@ def parse_stdout(self, stdout: str, *, exit_code: int = 0) -> AgentSessionResult
186190
"mcp_tool_calls": acc.mcp_tool_calls,
187191
"file_changes": acc.file_changes,
188192
"error_code": acc.error_code,
193+
"ndjson_unknown_event_count": acc.ndjson_unknown_event_count,
194+
"ndjson_unknown_item_count": acc.ndjson_unknown_item_count,
189195
},
190196
)
191197

@@ -200,6 +206,8 @@ class CodexStreamParser:
200206

201207
completion_marker: str = ""
202208
_saw_marker: bool = field(default=False, init=False, repr=False)
209+
ndjson_unknown_event_count: int = field(default=0, init=False, repr=False)
210+
ndjson_unknown_item_count: int = field(default=0, init=False, repr=False)
203211

204212
def _check_marker_text(self, text: str) -> None:
205213
if self.completion_marker and _marker_is_standalone(text, self.completion_marker):
@@ -308,6 +316,7 @@ def parse_line(self, line: str) -> SessionEvent | None:
308316
has_marker=False,
309317
)
310318

319+
self.ndjson_unknown_item_count += 1
311320
logger.warning("codex_ndjson_unknown_item_type", item_type=item.get("type", ""))
312321
return SessionEvent(
313322
kind=BackendEventKind.IGNORED,
@@ -362,6 +371,7 @@ def parse_line(self, line: str) -> SessionEvent | None:
362371
has_marker=False,
363372
)
364373

374+
self.ndjson_unknown_event_count += 1
365375
logger.warning("codex_ndjson_unknown_event_type", type=obj.get("type", ""))
366376
return SessionEvent(
367377
kind=BackendEventKind.IGNORED,

src/autoskillit/execution/headless/_headless_evidence.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ def _adapt_agent_result(agent_result: AgentSessionResult) -> ClaudeSessionResult
125125

126126
assistant_messages: list[str] = raw.get("agent_messages", [])
127127

128+
seen_ndjson_unknown_event_count: int = raw.get("ndjson_unknown_event_count", 0)
129+
seen_ndjson_unknown_item_count: int = raw.get("ndjson_unknown_item_count", 0)
130+
128131
return ClaudeSessionResult(
129132
subtype=subtype,
130133
is_error=is_error,
@@ -139,6 +142,8 @@ def _adapt_agent_result(agent_result: AgentSessionResult) -> ClaudeSessionResult
139142
has_thinking_only_turn=False,
140143
seen_block_types=frozenset(),
141144
api_error_status=api_error_status,
145+
seen_ndjson_unknown_event_count=seen_ndjson_unknown_event_count,
146+
seen_ndjson_unknown_item_count=seen_ndjson_unknown_item_count,
142147
)
143148

144149

src/autoskillit/execution/session/_session_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class ClaudeSessionResult:
7474
api_retry_last_status: int | None = None
7575
api_retry_exhausted: bool = False
7676
api_error_status: int | None = None
77+
seen_ndjson_unknown_event_count: int = 0
78+
seen_ndjson_unknown_item_count: int = 0
7779

7880
def __post_init__(self) -> None:
7981
if not isinstance(self.result, str):

tests/execution/backends/test_codex_result_parser.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def test_scan_empty_stdout_returns_empty_accumulator(self) -> None:
101101
assert acc.success is False
102102
assert acc.error_message == ""
103103
assert acc.error_code == ""
104+
assert acc.ndjson_unknown_event_count == 0
105+
assert acc.ndjson_unknown_item_count == 0
104106

105107
def test_scan_thread_started_populates_session_id(self) -> None:
106108
acc = _scan_codex_ndjson(_thread_started_line("t1"))
@@ -182,6 +184,16 @@ def test_scan_turn_failed_after_completed_overrides_success(self) -> None:
182184
assert acc.success is False
183185
assert acc.error_message == "error after success"
184186

187+
def test_scan_codex_ndjson_accumulator_counters_increment_independently(self) -> None:
188+
unknown_event_line = json.dumps({"type": "brand_new_event_type", "data": 1})
189+
unknown_item_line = json.dumps(
190+
{"type": "item.completed", "item": {"type": "brand_new_item_type", "data": 1}}
191+
)
192+
ndjson = unknown_event_line + "\n" + unknown_item_line
193+
acc = _scan_codex_ndjson(ndjson)
194+
assert acc.ndjson_unknown_event_count == 1
195+
assert acc.ndjson_unknown_item_count == 1
196+
185197

186198
class TestCodexResultParserStdout:
187199
def test_parse_stdout_empty_returns_error(self) -> None:
@@ -289,6 +301,8 @@ def test_parse_stdout_raw_contains_all_fields(self) -> None:
289301
assert raw["mcp_tool_calls"][0]["tool_name"] == "mcp_tool"
290302
assert raw["file_changes"] == ["/path/file.py"]
291303
assert raw["error_code"] == ""
304+
assert raw["ndjson_unknown_event_count"] == 0
305+
assert raw["ndjson_unknown_item_count"] == 0
292306

293307
def test_parse_result_completion_events_yield_success(self) -> None:
294308
parser = CodexResultParser()
@@ -759,6 +773,31 @@ def test_agent_message_populates_agent_messages(self) -> None:
759773
assert result.raw["agent_messages"] == ["hello from v0.136"]
760774
assert result.output == "hello from v0.136"
761775

776+
def test_ndjson_unknown_counters_in_raw_on_success(self) -> None:
777+
"""Normal success NDJSON stream → both counter keys present with value 0."""
778+
ndjson = "\n".join(
779+
[
780+
_thread_started_line("s1"),
781+
_item_completed_message_line("hello"),
782+
_turn_completed_line({"input_tokens": 1, "output_tokens": 1}),
783+
]
784+
)
785+
result = CodexResultParser().parse_stdout(ndjson)
786+
assert result.raw["ndjson_unknown_event_count"] == 0
787+
assert result.raw["ndjson_unknown_item_count"] == 0
788+
789+
def test_ndjson_unknown_counters_in_raw_on_failure(self) -> None:
790+
"""Failure NDJSON stream → both counter keys present with value 0."""
791+
ndjson = "\n".join(
792+
[
793+
_thread_started_line("s1"),
794+
_turn_failed_line("something broke"),
795+
]
796+
)
797+
result = CodexResultParser().parse_stdout(ndjson)
798+
assert result.raw["ndjson_unknown_event_count"] == 0
799+
assert result.raw["ndjson_unknown_item_count"] == 0
800+
762801
def test_command_execution_populates_command_executions(self) -> None:
763802
ndjson = "\n".join(
764803
[

tests/execution/backends/test_codex_stream_parser.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,36 @@ def test_unknown_event_type_still_warns_after_item_updated_guard(self) -> None:
605605
log["event"] == "codex_ndjson_unknown_event_type" and log["log_level"] == "warning"
606606
for log in cap
607607
)
608+
609+
610+
class TestCodexUnknownCounters:
611+
def test_stream_parser_event_counter_increments_on_unknown_event_type(self) -> None:
612+
parser = CodexStreamParser()
613+
line = json.dumps({"type": "brand_new_event_type", "data": 1})
614+
parser.parse_line(line)
615+
assert parser.ndjson_unknown_event_count == 1
616+
assert parser.ndjson_unknown_item_count == 0
617+
618+
def test_stream_parser_item_counter_increments_on_unknown_item_type(self) -> None:
619+
parser = CodexStreamParser()
620+
line = json.dumps(
621+
{"type": "item.completed", "item": {"type": "brand_new_item_type", "data": 1}}
622+
)
623+
parser.parse_line(line)
624+
assert parser.ndjson_unknown_item_count == 1
625+
assert parser.ndjson_unknown_event_count == 0
626+
627+
def test_counters_stay_zero_for_known_types(self) -> None:
628+
parser = CodexStreamParser()
629+
parser.parse_line(json.dumps({"type": "thread.started", "thread_id": "t1"}))
630+
parser.parse_line(
631+
json.dumps(
632+
{
633+
"type": "item.completed",
634+
"item": {"type": "message", "content": [{"type": "text", "text": "hi"}]},
635+
}
636+
)
637+
)
638+
parser.parse_line(json.dumps({"type": "turn.completed", "usage": {}}))
639+
assert parser.ndjson_unknown_event_count == 0
640+
assert parser.ndjson_unknown_item_count == 0

tests/execution/test_adapt_agent_result.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def test_hardcoded_thinking_defaults() -> None:
5858
result = _adapt_agent_result(_make_agent_result())
5959
assert result.has_thinking_only_turn is False
6060
assert result.seen_block_types == frozenset()
61+
assert result.seen_ndjson_unknown_event_count == 0
62+
assert result.seen_ndjson_unknown_item_count == 0
6163

6264

6365
def test_unknown_subtype_maps_to_unknown() -> None:
@@ -259,6 +261,8 @@ def test_unused_agent_fields_do_not_affect_output() -> None:
259261
assert base.stop_reasons == varied.stop_reasons
260262
assert base.has_thinking_only_turn == varied.has_thinking_only_turn
261263
assert base.seen_block_types == varied.seen_block_types
264+
assert base.seen_ndjson_unknown_event_count == varied.seen_ndjson_unknown_event_count
265+
assert base.seen_ndjson_unknown_item_count == varied.seen_ndjson_unknown_item_count
262266

263267
assert not hasattr(base, "exit_code")
264268
assert not hasattr(base, "backend_name")
@@ -296,6 +300,8 @@ def test_full_round_trip_all_fields() -> None:
296300
"mcp_tool_calls": [{"name": "read", "path": "/tmp"}],
297301
"file_changes": ["main.py"],
298302
"agent_messages": ["I updated the file."],
303+
"ndjson_unknown_event_count": 5,
304+
"ndjson_unknown_item_count": 2,
299305
},
300306
)
301307
result = _adapt_agent_result(agent)
@@ -317,6 +323,8 @@ def test_full_round_trip_all_fields() -> None:
317323
assert result.has_thinking_only_turn is False
318324
assert result.seen_block_types == frozenset()
319325
assert result.api_error_status is None
326+
assert result.seen_ndjson_unknown_event_count == 5
327+
assert result.seen_ndjson_unknown_item_count == 2
320328

321329

322330
def test_context_exhaustion_from_code_field() -> None:
@@ -396,6 +404,24 @@ def test_classify_infra_exit_rate_limited_via_adapted_error_code() -> None:
396404
assert exit_category == InfraExitCategory.RATE_LIMITED
397405

398406

407+
def test_ndjson_unknown_event_count_round_trip() -> None:
408+
result = _adapt_agent_result(_make_agent_result(raw={"ndjson_unknown_event_count": 3}))
409+
assert result.seen_ndjson_unknown_event_count == 3
410+
411+
412+
def test_ndjson_unknown_item_count_round_trip() -> None:
413+
result = _adapt_agent_result(_make_agent_result(raw={"ndjson_unknown_item_count": 7}))
414+
assert result.seen_ndjson_unknown_item_count == 7
415+
416+
417+
def test_ndjson_unknown_both_counters_present() -> None:
418+
result = _adapt_agent_result(
419+
_make_agent_result(raw={"ndjson_unknown_event_count": 3, "ndjson_unknown_item_count": 7})
420+
)
421+
assert result.seen_ndjson_unknown_event_count == 3
422+
assert result.seen_ndjson_unknown_item_count == 7
423+
424+
399425
class TestAdaptAgentResultFilePathKey:
400426
"""Verify file_change entries use 'file_path' key for synthesis compatibility."""
401427

0 commit comments

Comments
 (0)