cctally 1.77.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,8 @@ from __future__ import annotations
16
16
  import dataclasses
17
17
  import hashlib
18
18
  import json
19
+ import math
20
+ import re
19
21
  from typing import Any, Iterable
20
22
 
21
23
  # Reuse the Claude display helpers by IMPORT (never move them). _strip_ansi lives
@@ -65,6 +67,13 @@ CODEX_TEXT_CAP = 16000
65
67
  # Display title cap. Equal-by-test to the Claude first-prompt titler's _TITLE_MAX.
66
68
  CODEX_TITLE_MAX = _TITLE_MAX
67
69
 
70
+ # Bumped when the normalized row/item contract changes in a way that requires
71
+ # deterministic replay of retained Codex events. ``sync_codex_conversations``
72
+ # stores this in conversations.db only after a successful full sync; a missing
73
+ # or older value arms the existing byte-zero rebuild path without a schema
74
+ # migration (the store is wholly re-derivable).
75
+ CODEX_CONVERSATION_CONTRACT_VERSION = "5"
76
+
68
77
  # Structural wrapper prefixes skipped during title selection (§4.3), pinned from
69
78
  # the corpus (title-wrapper-window). Prefix-structural, never content heuristics.
70
79
  CODEX_TITLE_SKIP_PREFIXES: tuple[str, ...] = (
@@ -72,7 +81,7 @@ CODEX_TITLE_SKIP_PREFIXES: tuple[str, ...] = (
72
81
  "<user_instructions>",
73
82
  )
74
83
 
75
- _PROSE_KINDS = frozenset({"user", "assistant", "reasoning"})
84
+ _MIRROR_KINDS = frozenset({"user", "assistant", "reasoning", "meta"})
76
85
 
77
86
 
78
87
  def _cap(text: str) -> tuple[str, bool]:
@@ -175,6 +184,11 @@ def _canonical_json(value: Any) -> str:
175
184
  return ""
176
185
 
177
186
 
187
+ def _reject_json_constant(value: str) -> None:
188
+ """Reject NaN/Infinity tokens so card JSON stays standards-compliant."""
189
+ raise ValueError(f"non-finite JSON constant: {value}")
190
+
191
+
178
192
  def _stringify(value: Any) -> str:
179
193
  if value is None:
180
194
  return ""
@@ -196,6 +210,636 @@ def _join_content_texts(content: Any) -> str:
196
210
  return "\n".join(parts)
197
211
 
198
212
 
213
+ # ── card-ready native tool shaping (#331 Task A) ─────────────────────────────
214
+
215
+ CODEX_CARD_SCHEMA_VERSION = 1
216
+ _CARD_MAX_COMMANDS = 8
217
+ _CARD_MAX_PARTS = 128
218
+ _CARD_MAX_FILES = 128
219
+ _CARD_MAX_RESULTS = 50
220
+ _CARD_MAX_COLLECTION = 64
221
+ _CARD_HARNESS_PARSE_CAP = 1_000_000
222
+ _AGENT_OPERATIONS = frozenset({
223
+ "spawn_agent", "wait_agent", "send_message", "list_agents",
224
+ "followup_task", "interrupt_agent",
225
+ })
226
+ _EXEC_METADATA_KEYS = frozenset({
227
+ "justification", "login", "max_output_tokens", "prefix_rule",
228
+ "sandbox_permissions", "shell", "tty", "yield_time_ms",
229
+ })
230
+ _HARNESS_STATUS_RE = re.compile(
231
+ r"\A(Script completed|Script failed)\nWall time ([^\n]+)\n\nOutput:\Z")
232
+
233
+
234
+ class _LiteralError(ValueError):
235
+ """Closed-parser rejection; callers preserve the raw provider payload."""
236
+
237
+
238
+ class _TextBudget:
239
+ def __init__(self, limit: int):
240
+ self.remaining = max(0, int(limit))
241
+ self.truncated = False
242
+
243
+ def take(self, value: str) -> str:
244
+ if len(value) <= self.remaining:
245
+ self.remaining -= len(value)
246
+ return value
247
+ kept = value[:self.remaining]
248
+ self.remaining = 0
249
+ self.truncated = True
250
+ return kept
251
+
252
+
253
+ class _HarnessLiteralParser:
254
+ """Tiny non-executing parser for the JSON-like values in Codex harnesses.
255
+
256
+ It accepts only objects, arrays, double-quoted JSON strings, finite JSON
257
+ numbers, booleans and null. Object keys may be unquoted harness tokens
258
+ (including ``yield_time-ms``-style hyphens). Expressions, identifiers as
259
+ values, templates, comments and trailing code reject the shape.
260
+ """
261
+
262
+ def __init__(self, source: str, pos: int = 0, *, max_depth: int = 8):
263
+ self.source = source
264
+ self.pos = pos
265
+ self.max_depth = max_depth
266
+
267
+ def _ws(self) -> None:
268
+ size = len(self.source)
269
+ while self.pos < size and self.source[self.pos].isspace():
270
+ self.pos += 1
271
+
272
+ def _consume(self, token: str) -> None:
273
+ self._ws()
274
+ if not self.source.startswith(token, self.pos):
275
+ raise _LiteralError(token)
276
+ self.pos += len(token)
277
+
278
+ def _string(self) -> str:
279
+ self._ws()
280
+ if self.pos >= len(self.source) or self.source[self.pos] != '"':
281
+ raise _LiteralError("string")
282
+ try:
283
+ value, end = json.JSONDecoder().raw_decode(self.source, self.pos)
284
+ except (json.JSONDecodeError, TypeError) as exc:
285
+ raise _LiteralError("string") from exc
286
+ if not isinstance(value, str):
287
+ raise _LiteralError("string")
288
+ self.pos = end
289
+ return value
290
+
291
+ def _key(self) -> str:
292
+ self._ws()
293
+ if self.pos < len(self.source) and self.source[self.pos] == '"':
294
+ return self._string()
295
+ match = re.match(r"[A-Za-z_$][A-Za-z0-9_$-]*", self.source[self.pos:])
296
+ if match is None:
297
+ raise _LiteralError("key")
298
+ self.pos += len(match.group(0))
299
+ return match.group(0)
300
+
301
+ def value(self, depth: int = 0) -> Any:
302
+ if depth > self.max_depth:
303
+ raise _LiteralError("depth")
304
+ self._ws()
305
+ if self.pos >= len(self.source):
306
+ raise _LiteralError("value")
307
+ char = self.source[self.pos]
308
+ if char == '"':
309
+ return self._string()
310
+ if char == "{":
311
+ self.pos += 1
312
+ result: dict[str, Any] = {}
313
+ self._ws()
314
+ if self.pos < len(self.source) and self.source[self.pos] == "}":
315
+ self.pos += 1
316
+ return result
317
+ while len(result) < 64:
318
+ key = self._key()
319
+ if key in result:
320
+ raise _LiteralError("duplicate key")
321
+ self._consume(":")
322
+ result[key] = self.value(depth + 1)
323
+ self._ws()
324
+ if self.pos < len(self.source) and self.source[self.pos] == "}":
325
+ self.pos += 1
326
+ return result
327
+ self._consume(",")
328
+ raise _LiteralError("object size")
329
+ if char == "[":
330
+ self.pos += 1
331
+ result_list: list[Any] = []
332
+ self._ws()
333
+ if self.pos < len(self.source) and self.source[self.pos] == "]":
334
+ self.pos += 1
335
+ return result_list
336
+ while len(result_list) < 64:
337
+ result_list.append(self.value(depth + 1))
338
+ self._ws()
339
+ if self.pos < len(self.source) and self.source[self.pos] == "]":
340
+ self.pos += 1
341
+ return result_list
342
+ self._consume(",")
343
+ raise _LiteralError("array size")
344
+ for token, value in (("true", True), ("false", False), ("null", None)):
345
+ if self.source.startswith(token, self.pos):
346
+ self.pos += len(token)
347
+ return value
348
+ match = re.match(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?",
349
+ self.source[self.pos:])
350
+ if match is None:
351
+ raise _LiteralError("literal")
352
+ token = match.group(0)
353
+ if len(token) > 256:
354
+ raise _LiteralError("number length")
355
+ self.pos += len(token)
356
+ try:
357
+ value = float(token) if any(c in token for c in ".eE") else int(token)
358
+ except (OverflowError, ValueError) as exc:
359
+ raise _LiteralError("number") from exc
360
+ if isinstance(value, float) and not (-float("inf") < value < float("inf")):
361
+ raise _LiteralError("finite number")
362
+ return value
363
+
364
+
365
+ def _bounded_metadata(value: Any, budget: _TextBudget) -> Any:
366
+ if isinstance(value, str):
367
+ return budget.take(value)
368
+ if isinstance(value, (bool, int, float)):
369
+ return value
370
+ if (isinstance(value, list) and len(value) <= 32
371
+ and all(isinstance(part, str) for part in value)):
372
+ return [budget.take(part) for part in value]
373
+ return None
374
+
375
+
376
+ def _bounded_json(value: Any, budget: _TextBudget, *, depth: int = 0) -> Any:
377
+ """Retain JSON structure under one shared text/shape budget.
378
+
379
+ Native payload readback remains authoritative; this projection is only the
380
+ bounded card wire. Collection overflow is explicit through the caller's
381
+ ``budget.truncated`` flag rather than silently expanding the detail route.
382
+ """
383
+ if depth > 8:
384
+ budget.truncated = True
385
+ return None
386
+ if isinstance(value, str):
387
+ return budget.take(value)
388
+ if value is None or isinstance(value, (bool, int)):
389
+ return value
390
+ if isinstance(value, float):
391
+ if math.isfinite(value):
392
+ return value
393
+ budget.truncated = True
394
+ return None
395
+ if isinstance(value, list):
396
+ if len(value) > _CARD_MAX_COLLECTION:
397
+ budget.truncated = True
398
+ return [
399
+ _bounded_json(part, budget, depth=depth + 1)
400
+ for part in value[:_CARD_MAX_COLLECTION]
401
+ ]
402
+ if isinstance(value, dict):
403
+ if len(value) > _CARD_MAX_COLLECTION:
404
+ budget.truncated = True
405
+ result = {}
406
+ for key, part in list(value.items())[:_CARD_MAX_COLLECTION]:
407
+ if not isinstance(key, str):
408
+ budget.truncated = True
409
+ continue
410
+ result[budget.take(key)] = _bounded_json(
411
+ part, budget, depth=depth + 1)
412
+ return result
413
+ budget.truncated = True
414
+ return None
415
+
416
+
417
+ def _json_object(value: Any) -> dict | None:
418
+ if isinstance(value, dict):
419
+ return value
420
+ if not isinstance(value, str) or len(value) > _CARD_HARNESS_PARSE_CAP:
421
+ return None
422
+ try:
423
+ parsed = json.loads(value, parse_constant=_reject_json_constant)
424
+ except (json.JSONDecodeError, TypeError, ValueError):
425
+ return None
426
+ return parsed if isinstance(parsed, dict) else None
427
+
428
+
429
+ def decode_secondary_tool_call_card(
430
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
431
+ ) -> dict | None:
432
+ """Bounded additive wire for plans, web actions, and agent operations.
433
+
434
+ Unknown or malformed shapes deliberately return ``None`` so the existing
435
+ provider-name + raw-argument fallback remains visible and payload readback
436
+ stays authoritative.
437
+ """
438
+ if not isinstance(payload, dict):
439
+ return None
440
+ ptype = payload.get("type")
441
+ name = payload.get("name")
442
+ status = payload.get("status") if isinstance(payload.get("status"), str) else "requested"
443
+ budget = _TextBudget(text_cap)
444
+ if ptype == "function_call" and name == "update_plan":
445
+ arguments = _json_object(payload.get("arguments"))
446
+ plan = arguments.get("plan") if isinstance(arguments, dict) else None
447
+ if not isinstance(plan, list) or len(plan) > _CARD_MAX_COLLECTION:
448
+ return None
449
+ items = []
450
+ for item in plan:
451
+ if not (isinstance(item, dict)
452
+ and isinstance(item.get("step"), str)
453
+ and isinstance(item.get("status"), str)):
454
+ return None
455
+ items.append({
456
+ "step": budget.take(item["step"]),
457
+ "status": budget.take(item["status"]),
458
+ })
459
+ card = {
460
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
461
+ "type": "plan", "source": "update_plan",
462
+ "call_status": status, "items": items,
463
+ }
464
+ explanation = arguments.get("explanation")
465
+ if explanation is not None:
466
+ if not isinstance(explanation, str):
467
+ return None
468
+ card["explanation"] = budget.take(explanation)
469
+ if budget.truncated:
470
+ card["truncated"] = True
471
+ return card
472
+ if ptype == "web_search_call":
473
+ action = payload.get("action")
474
+ if not isinstance(action, (dict, str)):
475
+ return None
476
+ bounded_action = _bounded_json(action, budget)
477
+ card = {
478
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
479
+ "type": "web_search", "source": "web_search_call",
480
+ "call_status": status, "action": bounded_action,
481
+ }
482
+ if isinstance(action, dict) and isinstance(action.get("query"), str):
483
+ card["query"] = budget.take(action["query"])
484
+ if budget.truncated:
485
+ card["truncated"] = True
486
+ return card
487
+ if ptype == "function_call" and name in _AGENT_OPERATIONS:
488
+ arguments = _json_object(payload.get("arguments"))
489
+ if arguments is None:
490
+ return None
491
+ bounded = _bounded_json(arguments, budget)
492
+ card = {
493
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
494
+ "type": "agent", "operation": name,
495
+ "call_status": status, "arguments": bounded,
496
+ }
497
+ if budget.truncated:
498
+ card["truncated"] = True
499
+ return card
500
+ return None
501
+
502
+
503
+ def decode_secondary_tool_result(
504
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
505
+ ) -> dict | None:
506
+ if not isinstance(payload, dict) or payload.get("type") not in _RESPONSE_TOOL_OUTPUTS:
507
+ return None
508
+ value = payload["output"] if "output" in payload else payload.get("tools")
509
+ if isinstance(value, str) and len(value) <= _CARD_HARNESS_PARSE_CAP:
510
+ try:
511
+ parsed = json.loads(value, parse_constant=_reject_json_constant)
512
+ except (json.JSONDecodeError, TypeError, ValueError):
513
+ parsed = value
514
+ else:
515
+ parsed = value
516
+ budget = _TextBudget(text_cap)
517
+ bounded = _bounded_json(parsed, budget)
518
+ status = payload.get("status") if isinstance(payload.get("status"), str) else "returned"
519
+ return {"status": status, "value": bounded, "truncated": budget.truncated}
520
+
521
+
522
+ def decode_secondary_event_card(
523
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
524
+ ) -> dict | None:
525
+ if not isinstance(payload, dict):
526
+ return None
527
+ ptype = payload.get("type")
528
+ budget = _TextBudget(text_cap)
529
+ if ptype == "web_search_end":
530
+ action = payload.get("action")
531
+ query = payload.get("query")
532
+ results = payload.get("results", [])
533
+ if not isinstance(action, (dict, str)) or not isinstance(query, str):
534
+ return None
535
+ if not isinstance(results, list):
536
+ return None
537
+ if len(results) > _CARD_MAX_RESULTS:
538
+ budget.truncated = True
539
+ card = {
540
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
541
+ "type": "web_search_completion", "source": "web_search_end",
542
+ "status": (payload.get("status")
543
+ if isinstance(payload.get("status"), str) else "returned"),
544
+ "query": budget.take(query),
545
+ "action": _bounded_json(action, budget),
546
+ "results": [
547
+ _bounded_json(result, budget)
548
+ for result in results[:_CARD_MAX_RESULTS]
549
+ ],
550
+ }
551
+ if "error" in payload:
552
+ card["error"] = _bounded_json(payload.get("error"), budget)
553
+ if budget.truncated:
554
+ card["truncated"] = True
555
+ return card
556
+ if ptype == "mcp_tool_call_end":
557
+ invocation = payload.get("invocation")
558
+ if not isinstance(invocation, dict):
559
+ return None
560
+ server = invocation.get("server")
561
+ tool = invocation.get("tool") or invocation.get("name")
562
+ if server is not None and not isinstance(server, str):
563
+ return None
564
+ if not isinstance(tool, str):
565
+ return None
566
+ result = payload.get("result")
567
+ if isinstance(result, dict) and "Ok" in result:
568
+ status = "ok"
569
+ elif isinstance(result, dict) and any(key in result for key in ("Err", "Error")):
570
+ status = "error"
571
+ else:
572
+ status = (payload.get("status")
573
+ if isinstance(payload.get("status"), str) else "returned")
574
+ card = {
575
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
576
+ "type": "mcp_completion", "source": "mcp_tool_call_end",
577
+ "status": status,
578
+ "server": budget.take(server) if isinstance(server, str) else None,
579
+ "tool": budget.take(tool),
580
+ "arguments": _bounded_json(invocation.get("arguments"), budget),
581
+ "result": _bounded_json(result, budget),
582
+ "duration": _bounded_json(payload.get("duration"), budget),
583
+ }
584
+ if budget.truncated:
585
+ card["truncated"] = True
586
+ return card
587
+ return None
588
+
589
+
590
+ def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
591
+ """Decode only the complete current exec harness statement grammar."""
592
+ commands: list[dict] = []
593
+ pos = 0
594
+ while True:
595
+ prefix = re.match(
596
+ r"\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*"
597
+ r"await\s+tools\.exec_command",
598
+ source[pos:],
599
+ )
600
+ if prefix is None or len(commands) >= _CARD_MAX_COMMANDS:
601
+ return None
602
+ variable = prefix.group(1)
603
+ parser = _HarnessLiteralParser(source, pos + prefix.end())
604
+ try:
605
+ parser._consume("(")
606
+ value = parser.value()
607
+ parser._consume(")")
608
+ except _LiteralError:
609
+ return None
610
+ if not isinstance(value, dict) or not isinstance(value.get("cmd"), str):
611
+ return None
612
+ workdir = value.get("workdir")
613
+ if workdir is not None and not isinstance(workdir, str):
614
+ return None
615
+ command = {
616
+ "workdir": budget.take(workdir) if isinstance(workdir, str) else None,
617
+ "command": "",
618
+ "metadata": {},
619
+ }
620
+ command["command"] = budget.take(value["cmd"])
621
+ for key in sorted(_EXEC_METADATA_KEYS):
622
+ if key not in value:
623
+ continue
624
+ bounded = _bounded_metadata(value[key], budget)
625
+ if bounded is not None:
626
+ command["metadata"][key] = bounded
627
+ commands.append(command)
628
+ suffix = re.match(
629
+ r"\s*;\s*text\s*\(\s*" + re.escape(variable)
630
+ + r"\.output\s*\)\s*;?",
631
+ source[parser.pos:],
632
+ )
633
+ if suffix is None:
634
+ return None
635
+ pos = parser.pos + suffix.end()
636
+ if not source[pos:].strip():
637
+ return commands
638
+
639
+
640
+ def _decode_apply_patch_program(source: str) -> str | None:
641
+ """Recognize the exact current ``const patch`` apply_patch harness."""
642
+ match = re.match(r"\A\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*", source)
643
+ if match is None:
644
+ return None
645
+ parser = _HarnessLiteralParser(source, match.end())
646
+ try:
647
+ patch = parser._string()
648
+ except _LiteralError:
649
+ return None
650
+ variable = match.group(1)
651
+ tail = source[parser.pos:]
652
+ pattern = (
653
+ r"\s*;\s*text\s*\(\s*await\s+tools\.apply_patch\s*\(\s*"
654
+ + re.escape(variable)
655
+ + r"\s*\)\s*\)\s*;?\s*\Z"
656
+ )
657
+ return patch if re.fullmatch(pattern, tail) else None
658
+
659
+
660
+ def _apply_patch_heredoc(command: str) -> str | None:
661
+ for pattern in (
662
+ r"\A\s*apply_patch\s+<<'([A-Za-z_][A-Za-z0-9_]*)'\n(.*)\n\1\s*\Z",
663
+ r'\A\s*apply_patch\s+<<"([A-Za-z_][A-Za-z0-9_]*)"\n(.*)\n\1\s*\Z',
664
+ r"\A\s*apply_patch\s+<<([A-Za-z_][A-Za-z0-9_]*)\n(.*)\n\1\s*\Z",
665
+ ):
666
+ match = re.fullmatch(pattern, command, re.DOTALL)
667
+ if match is not None:
668
+ return match.group(2)
669
+ return None
670
+
671
+
672
+ def _patch_files_from_apply_patch(
673
+ patch: str, budget: _TextBudget | None = None,
674
+ ) -> list[dict]:
675
+ files: list[dict] = []
676
+ for line in patch.splitlines():
677
+ match = re.match(r"\*\*\* (Add|Update|Delete) File: (.+)\Z", line)
678
+ if match is not None and len(files) < _CARD_MAX_FILES:
679
+ status = {"Add": "added", "Update": "modified", "Delete": "deleted"}[
680
+ match.group(1)]
681
+ path = budget.take(match.group(2)) if budget is not None else match.group(2)
682
+ files.append({"path": path, "status": status})
683
+ continue
684
+ move = re.match(r"\*\*\* Move to: (.+)\Z", line)
685
+ if move is not None and files:
686
+ files[-1]["move_path"] = (
687
+ budget.take(move.group(1)) if budget is not None else move.group(1))
688
+ files[-1]["status"] = "moved"
689
+ return files
690
+
691
+
692
+ def _complete_apply_patch(patch: str) -> bool:
693
+ """Require one closed apply_patch envelope with at least one file action."""
694
+ lines = patch.splitlines()
695
+ if (not lines or lines[0] != "*** Begin Patch"
696
+ or lines[-1] != "*** End Patch"):
697
+ return False
698
+ if lines.count("*** Begin Patch") != 1 or lines.count("*** End Patch") != 1:
699
+ return False
700
+ return any(re.fullmatch(r"\*\*\* (?:Add|Update|Delete) File: .+", line)
701
+ for line in lines[1:-1])
702
+
703
+
704
+ def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
705
+ """Return the additive card contract for a structurally proven call."""
706
+ if not isinstance(payload, dict) or payload.get("type") != "custom_tool_call":
707
+ return None
708
+ name = payload.get("name")
709
+ value = payload.get("input")
710
+ if not isinstance(name, str) or not isinstance(value, str):
711
+ return None
712
+ if len(value) > _CARD_HARNESS_PARSE_CAP:
713
+ return None
714
+ budget = _TextBudget(text_cap)
715
+ status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
716
+ if name == "apply_patch" and _complete_apply_patch(value):
717
+ files = _patch_files_from_apply_patch(value, budget)
718
+ patch = budget.take(value)
719
+ return {
720
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
721
+ "type": "patch", "source": "apply_patch", "status": status,
722
+ "patch": patch, "files": files,
723
+ "truncated": budget.truncated,
724
+ }
725
+ if name != "exec":
726
+ return None
727
+ patch_value = _decode_apply_patch_program(value)
728
+ if patch_value is not None and _complete_apply_patch(patch_value):
729
+ files = _patch_files_from_apply_patch(patch_value, budget)
730
+ patch = budget.take(patch_value)
731
+ return {
732
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
733
+ "type": "patch", "source": "tools.apply_patch", "status": status,
734
+ "patch": patch, "files": files,
735
+ "truncated": budget.truncated,
736
+ }
737
+ commands = _exec_invocations(value, budget=budget)
738
+ if commands is None:
739
+ return None
740
+ if len(commands) == 1:
741
+ heredoc = _apply_patch_heredoc(commands[0]["command"])
742
+ if heredoc is not None and _complete_apply_patch(heredoc):
743
+ files = _patch_files_from_apply_patch(heredoc, budget)
744
+ return {
745
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
746
+ "type": "patch", "source": "exec_apply_patch", "status": status,
747
+ "patch": budget.take(heredoc), "files": files,
748
+ "workdir": commands[0]["workdir"], "truncated": budget.truncated,
749
+ }
750
+ card = {
751
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
752
+ "type": "terminal", "status": status, "commands": commands,
753
+ }
754
+ if budget.truncated:
755
+ card["truncated"] = True
756
+ return card
757
+
758
+
759
+ def decode_tool_output_card(
760
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
761
+ ) -> tuple[dict, str] | None:
762
+ """Unwrap supported output envelopes without losing malformed parts."""
763
+ if not isinstance(payload, dict) or payload.get("type") not in _RESPONSE_TOOL_OUTPUTS:
764
+ return None
765
+ value = payload["output"] if "output" in payload else payload.get("tools")
766
+ values = value if isinstance(value, list) else [value]
767
+ budget = _TextBudget(text_cap)
768
+ parts: list[dict] = []
769
+ status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
770
+ for index, part in enumerate(values[:_CARD_MAX_PARTS]):
771
+ if isinstance(part, str):
772
+ text = part
773
+ match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
774
+ if match is not None:
775
+ status = "completed" if match.group(1) == "Script completed" else "failed"
776
+ continue
777
+ parts.append({"type": "text", "stream": "output", "text": budget.take(text)})
778
+ continue
779
+ if isinstance(part, dict) and part.get("type") == "input_text" \
780
+ and isinstance(part.get("text"), str):
781
+ text = part["text"]
782
+ match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
783
+ if match is not None:
784
+ status = "completed" if match.group(1) == "Script completed" else "failed"
785
+ continue
786
+ stream = part.get("stream") if part.get("stream") in {"stdout", "stderr"} else "output"
787
+ parts.append({"type": "text", "stream": stream, "text": budget.take(text)})
788
+ continue
789
+ raw = _canonical_json(part)
790
+ parts.append({"type": "raw", "stream": "output", "text": budget.take(raw)})
791
+ if len(values) > _CARD_MAX_PARTS:
792
+ budget.truncated = True
793
+ card = {
794
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
795
+ "type": "terminal_output", "status": status,
796
+ "is_error": status in {"failed", "error"},
797
+ "parts": parts, "truncated": budget.truncated,
798
+ }
799
+ return card, "".join(part["text"] for part in parts)
800
+
801
+
802
+ def decode_patch_event_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
803
+ """Bounded, provider-truthful ``patch_apply_end`` projection."""
804
+ if not isinstance(payload, dict) or payload.get("type") != "patch_apply_end":
805
+ return None
806
+ budget = _TextBudget(text_cap)
807
+ files: list[dict] = []
808
+ changes = payload.get("changes")
809
+ if isinstance(changes, list):
810
+ for change in changes[:_CARD_MAX_FILES]:
811
+ if not isinstance(change, dict):
812
+ files.append({"raw": budget.take(_canonical_json(change))})
813
+ continue
814
+ entry: dict[str, Any] = {}
815
+ for key in ("path", "move_path", "status"):
816
+ if isinstance(change.get(key), str):
817
+ entry[key] = budget.take(change[key])
818
+ if isinstance(change.get("unified_diff"), str):
819
+ entry["unified_diff"] = budget.take(change["unified_diff"])
820
+ unknown = {key: value for key, value in change.items()
821
+ if key not in {"path", "move_path", "status", "unified_diff"}}
822
+ if unknown:
823
+ entry["raw_extra"] = budget.take(_canonical_json(unknown))
824
+ files.append(entry)
825
+ if len(changes) > _CARD_MAX_FILES:
826
+ budget.truncated = True
827
+ elif "changes" in payload:
828
+ files.append({"raw": budget.take(_canonical_json(changes))})
829
+ stdout = budget.take(payload["stdout"]) if isinstance(payload.get("stdout"), str) else None
830
+ stderr = budget.take(payload["stderr"]) if isinstance(payload.get("stderr"), str) else None
831
+ status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
832
+ success = payload.get("success") if isinstance(payload.get("success"), bool) else None
833
+ return {
834
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
835
+ "type": "patch", "source": "patch_apply_end",
836
+ "files": files,
837
+ "has_diff": any(isinstance(entry.get("unified_diff"), str) for entry in files),
838
+ "status": status, "success": success, "stdout": stdout, "stderr": stderr,
839
+ "truncated": budget.truncated,
840
+ }
841
+
842
+
199
843
  @dataclasses.dataclass
200
844
  class _Extracted:
201
845
  kind: str
@@ -203,6 +847,334 @@ class _Extracted:
203
847
  column: str # "text" | "search_tool" | "search_thinking"
204
848
  detail: dict | None
205
849
  touches: list[tuple[str, str]] # (file_path, tool)
850
+ identity_text: str | None = None # raw pre-segmentation mirror/payload identity
851
+
852
+
853
+ _REASONING_TITLE_RE = re.compile(r"\A\*\*([^\n]+)\*\*\Z")
854
+ _MARKER_DIRECTIVE_RE = re.compile(r"\A::([a-z0-9-]+)\{(.*)\}\Z")
855
+ _MARKER_ATTR_RE = re.compile(
856
+ r'([A-Za-z][A-Za-z0-9]*)=("(?:[^"\\]|\\.)*"|true|false)')
857
+ _MARKER_DIRECTIVES = {
858
+ "git-create-branch": ({"cwd", "branch"}, "create_branch"),
859
+ "git-stage": ({"cwd"}, "stage"),
860
+ "git-commit": ({"cwd"}, "commit"),
861
+ "git-push": ({"cwd", "branch"}, "push"),
862
+ "git-create-pr": ({"cwd", "branch", "url", "isDraft"}, "create_pr"),
863
+ }
864
+ _MEM_CITATION_RE = re.compile(
865
+ r"\A[^<>\r\n]+:\d+-\d+\|note=\[[^\]\r\n]*\]\Z")
866
+ _ROLLOUT_ID_RE = re.compile(
867
+ r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z",
868
+ re.IGNORECASE,
869
+ )
870
+
871
+
872
+ def _reasoning_projection(
873
+ *, source: str, summary: str = "", body: str = "",
874
+ ) -> dict | None:
875
+ """Provider-truthful title/summary/body projection for non-empty reasoning."""
876
+ summary_nonblank = summary.strip()
877
+ body_nonblank = body.strip()
878
+ if not summary_nonblank and not body_nonblank:
879
+ return None
880
+ result = {"schema_version": 1, "source": source}
881
+ match = _REASONING_TITLE_RE.fullmatch(summary_nonblank)
882
+ if match is not None:
883
+ title = match.group(1)
884
+ if title.strip() == title and "**" not in title:
885
+ result["title"] = title
886
+ else:
887
+ result["summary"] = summary_nonblank
888
+ elif summary_nonblank:
889
+ result["summary"] = summary_nonblank
890
+ if body_nonblank:
891
+ result["body"] = body_nonblank
892
+ return result
893
+
894
+
895
+ def _parse_marker_directive(line: str) -> dict | None:
896
+ matched = _MARKER_DIRECTIVE_RE.fullmatch(line)
897
+ if matched is None or matched.group(1) not in _MARKER_DIRECTIVES:
898
+ return None
899
+ name, source = matched.group(1), matched.group(2)
900
+ attrs: dict[str, object] = {}
901
+ pos = 0
902
+ while pos < len(source):
903
+ while pos < len(source) and source[pos] == " ":
904
+ pos += 1
905
+ token = _MARKER_ATTR_RE.match(source, pos)
906
+ if token is None or token.group(1) in attrs:
907
+ return None
908
+ raw = token.group(2)
909
+ try:
910
+ value = json.loads(raw) if raw.startswith('"') else raw == "true"
911
+ except (json.JSONDecodeError, TypeError):
912
+ return None
913
+ attrs[token.group(1)] = value
914
+ pos = token.end()
915
+ if pos < len(source) and source[pos] != " ":
916
+ return None
917
+ required, action = _MARKER_DIRECTIVES[name]
918
+ if set(attrs) != required:
919
+ return None
920
+ if any(not isinstance(attrs[key], str) or not attrs[key]
921
+ for key in required - {"isDraft"}):
922
+ return None
923
+ if name == "git-create-pr":
924
+ if not isinstance(attrs.get("isDraft"), bool):
925
+ return None
926
+ if not str(attrs.get("url", "")).startswith(("https://", "http://")):
927
+ return None
928
+ marker = {"schema_version": 1, "type": "git", "action": action}
929
+ if name == "git-create-pr":
930
+ marker["draft"] = attrs["isDraft"]
931
+ return marker
932
+
933
+
934
+ def _parse_memory_citation(lines: list[str]) -> dict | None:
935
+ try:
936
+ citation_open = lines.index("<citation_entries>")
937
+ citation_close = lines.index("</citation_entries>")
938
+ rollout_open = lines.index("<rollout_ids>")
939
+ rollout_close = lines.index("</rollout_ids>")
940
+ except ValueError:
941
+ return None
942
+ if not (
943
+ lines[0] == "<oai-mem-citation>"
944
+ and lines[-1] == "</oai-mem-citation>"
945
+ and citation_open == 1
946
+ and citation_open < citation_close < rollout_open < rollout_close
947
+ and rollout_close == len(lines) - 2
948
+ and rollout_open == citation_close + 1
949
+ ):
950
+ return None
951
+ citations = lines[citation_open + 1:citation_close]
952
+ rollouts = lines[rollout_open + 1:rollout_close]
953
+ if not citations or not all(_MEM_CITATION_RE.fullmatch(line) for line in citations):
954
+ return None
955
+ if not all(_ROLLOUT_ID_RE.fullmatch(line) for line in rollouts):
956
+ return None
957
+ return {
958
+ "schema_version": 1, "type": "memory_citation",
959
+ "citation_count": len(citations), "rollout_count": len(rollouts),
960
+ }
961
+
962
+
963
+ def _segment_harness_markers(text: str) -> tuple[str, list[dict]]:
964
+ """Segment only a closed trailing suffix; authored/fenced lookalikes stay text."""
965
+ lines = text.splitlines()
966
+ end = len(lines)
967
+ while end and not lines[end - 1].strip():
968
+ end -= 1
969
+ reversed_markers: list[dict] = []
970
+ while end:
971
+ marker = _parse_marker_directive(lines[end - 1])
972
+ start = end - 1
973
+ if marker is None and lines[end - 1] == "</oai-mem-citation>":
974
+ starts = [index for index in range(end - 2, -1, -1)
975
+ if lines[index] == "<oai-mem-citation>"]
976
+ if starts:
977
+ start = starts[0]
978
+ marker = _parse_memory_citation(lines[start:end])
979
+ if marker is None:
980
+ break
981
+ # A suffix that begins inside an open Markdown fence is authored code.
982
+ fence: str | None = None
983
+ for line in lines[:start]:
984
+ opened = re.match(r"\s*(`{3,}|~{3,})", line)
985
+ if opened is None:
986
+ continue
987
+ char = opened.group(1)[0]
988
+ if fence is None:
989
+ fence = char
990
+ elif fence == char:
991
+ fence = None
992
+ if fence is not None:
993
+ break
994
+ reversed_markers.append(marker)
995
+ end = start
996
+ while end and not lines[end - 1].strip():
997
+ end -= 1
998
+ if not reversed_markers:
999
+ return text, []
1000
+ return "\n".join(lines[:end]).rstrip(), list(reversed(reversed_markers))
1001
+
1002
+
1003
+ _TASK_STARTED_KEYS = frozenset({
1004
+ "type", "collaboration_mode_kind", "model_context_window", "started_at", "turn_id",
1005
+ })
1006
+ _TASK_COMPLETE_KEYS = frozenset({
1007
+ "type", "completed_at", "duration_ms", "last_agent_message", "started_at",
1008
+ "time_to_first_token_ms", "turn_id",
1009
+ })
1010
+
1011
+
1012
+ def _lifecycle_projection(payload: dict) -> dict:
1013
+ """Safe lifecycle detail plus internal evidence used by canonical folding."""
1014
+ event = payload.get("type")
1015
+ public: dict[str, object] = {
1016
+ "schema_version": 1, "event": event,
1017
+ "state": "started" if event == "task_started" else "completed",
1018
+ }
1019
+ allowed = _TASK_STARTED_KEYS if event == "task_started" else _TASK_COMPLETE_KEYS
1020
+ valid = set(payload) <= allowed
1021
+ turn_id = payload.get("turn_id")
1022
+ valid = valid and isinstance(turn_id, str) and bool(turn_id)
1023
+ if event == "task_started":
1024
+ for src, dst in (
1025
+ ("started_at", "at"),
1026
+ ("collaboration_mode_kind", "collaboration_mode_kind"),
1027
+ ):
1028
+ value = payload.get(src)
1029
+ if value is not None:
1030
+ if src == "started_at" and (
1031
+ isinstance(value, (int, float)) and not isinstance(value, bool)
1032
+ and math.isfinite(value)
1033
+ ):
1034
+ public[dst] = value
1035
+ elif not isinstance(value, str):
1036
+ valid = False
1037
+ else:
1038
+ public[dst] = value
1039
+ window = payload.get("model_context_window")
1040
+ if window is not None:
1041
+ if not isinstance(window, int) or isinstance(window, bool):
1042
+ valid = False
1043
+ else:
1044
+ public["model_context_window"] = window
1045
+ return {"lifecycle": public, "_lifecycle_foldable": valid}
1046
+
1047
+ for src, dst in (("completed_at", "at"), ("started_at", "started_at")):
1048
+ value = payload.get(src)
1049
+ if value is not None:
1050
+ if (isinstance(value, (int, float)) and not isinstance(value, bool)
1051
+ and math.isfinite(value)):
1052
+ public[dst] = value
1053
+ elif not isinstance(value, str):
1054
+ valid = False
1055
+ else:
1056
+ public[dst] = value
1057
+ for key in ("duration_ms", "time_to_first_token_ms"):
1058
+ value = payload.get(key)
1059
+ if value is not None:
1060
+ if (not isinstance(value, (int, float)) or isinstance(value, bool)
1061
+ or not math.isfinite(value)):
1062
+ valid = False
1063
+ else:
1064
+ public[key] = value
1065
+ message = payload.get("last_agent_message")
1066
+ if message is not None and not isinstance(message, str):
1067
+ valid = False
1068
+ message = ""
1069
+ message = message or ""
1070
+ if message.strip():
1071
+ public["message"] = message[:CODEX_TEXT_CAP]
1072
+ error = payload.get("error")
1073
+ if error is not None:
1074
+ valid = False
1075
+ public["state"] = "failed"
1076
+ public["error"] = _stringify(error)[:CODEX_TEXT_CAP]
1077
+ return {
1078
+ "lifecycle": public,
1079
+ "_lifecycle_foldable": valid,
1080
+ "_lifecycle_message_digest": content_digest(message),
1081
+ "_lifecycle_message_len": content_len(message),
1082
+ }
1083
+
1084
+
1085
+ _SKILL_NAME_RE = re.compile(r"<name>\s*([^<\n]+?)\s*</name>", re.IGNORECASE)
1086
+ _AGENTS_ENVELOPE_RE = re.compile(
1087
+ r"\A\s*# AGENTS\.md instructions for [^\n]+\n\s*"
1088
+ r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*\Z",
1089
+ re.DOTALL,
1090
+ )
1091
+ _AGENTS_ENV_BUNDLE_RE = re.compile(
1092
+ r"\A\s*# AGENTS\.md instructions for [^\n]+\n\s*"
1093
+ r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*"
1094
+ r"<environment_context>.*?</environment_context>\s*\Z",
1095
+ re.DOTALL,
1096
+ )
1097
+ _CONTEXT_BUNDLE_RE = re.compile(
1098
+ r"\A\s*"
1099
+ r"<recommended_plugins>.*?</recommended_plugins>\s*"
1100
+ r"# AGENTS\.md instructions for [^\n]+\n\s*"
1101
+ r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*"
1102
+ r"<environment_context>.*?</environment_context>"
1103
+ r"\s*\Z",
1104
+ re.DOTALL,
1105
+ )
1106
+
1107
+
1108
+ def _exact_wrapper(text: str, opening: str, closing: str) -> bool:
1109
+ stripped = (text or "").strip()
1110
+ return stripped.startswith(opening) and stripped.endswith(closing)
1111
+
1112
+
1113
+ def _meta_detail(meta_kind: str, meta_label: str, text: str) -> dict:
1114
+ detail = {"meta_kind": meta_kind, "meta_label": meta_label}
1115
+ if meta_label == "skill":
1116
+ match = _SKILL_NAME_RE.search(text or "")
1117
+ if match:
1118
+ detail["skill_name"] = match.group(1).strip()
1119
+ return detail
1120
+
1121
+
1122
+ def _classify_injected_message(role: str | None, text: str) -> dict | None:
1123
+ """Return an explicit neutral meta descriptor for proven harness content.
1124
+
1125
+ Non-human roles are authoritative provider metadata and always context. A
1126
+ user/assistant row is reclassified only by a closed, structural wrapper
1127
+ shape; unknown XML and ordinary prose remain provider-authored content.
1128
+ """
1129
+ stripped = (text or "").strip()
1130
+ lower = stripped.lower()
1131
+
1132
+ # Some Codex hosts combine three independently injected envelopes into one
1133
+ # user-role message. Match the complete, ordered grammar so an appended
1134
+ # human prompt (or merely XML-looking prose) cannot be hidden as context.
1135
+ if role == "user" and _CONTEXT_BUNDLE_RE.fullmatch(text or ""):
1136
+ detail = _meta_detail("context", "context_bundle", text)
1137
+ detail["meta_sections"] = ["plugins", "agents", "environment"]
1138
+ return detail
1139
+ if role == "user" and _AGENTS_ENV_BUNDLE_RE.fullmatch(text or ""):
1140
+ detail = _meta_detail("context", "context_bundle", text)
1141
+ detail["meta_sections"] = ["agents", "environment"]
1142
+ return detail
1143
+
1144
+ if role not in (None, "user", "assistant"):
1145
+ if _exact_wrapper(lower, "<permissions instructions>",
1146
+ "</permissions instructions>"):
1147
+ return _meta_detail("context", "permissions", text)
1148
+ if _exact_wrapper(lower, "<multi_agent_mode>", "</multi_agent_mode>"):
1149
+ return _meta_detail("context", "mode", text)
1150
+ if _exact_wrapper(lower, "<codex_delegation>", "</codex_delegation>"):
1151
+ return _meta_detail("context", "delegation", text)
1152
+ if _exact_wrapper(lower, "<heartbeat>", "</heartbeat>"):
1153
+ return _meta_detail("notification", "heartbeat", text)
1154
+ if _exact_wrapper(lower, "<model_switch>", "</model_switch>"):
1155
+ return _meta_detail("notification", "model_switch", text)
1156
+ return _meta_detail("context", "role", text)
1157
+
1158
+ wrappers = (
1159
+ ("<environment_context>", "</environment_context>", "context", "environment"),
1160
+ ("<user_instructions>", "</user_instructions>", "context", "instructions"),
1161
+ ("<permissions instructions>", "</permissions instructions>", "context", "permissions"),
1162
+ ("<multi_agent_mode>", "</multi_agent_mode>", "context", "mode"),
1163
+ ("<codex_delegation>", "</codex_delegation>", "context", "delegation"),
1164
+ ("<heartbeat>", "</heartbeat>", "notification", "heartbeat"),
1165
+ ("<model_switch>", "</model_switch>", "notification", "model_switch"),
1166
+ ("<recommended_plugins>", "</recommended_plugins>", "context", "plugins"),
1167
+ ("<skill>", "</skill>", "skill", "skill"),
1168
+ ("<oai-mem-citation>", "</oai-mem-citation>", "notification", "memory"),
1169
+ ("<memory_context>", "</memory_context>", "context", "memory"),
1170
+ ("<memory-context>", "</memory-context>", "context", "memory"),
1171
+ )
1172
+ for opening, closing, meta_kind, label in wrappers:
1173
+ if _exact_wrapper(lower, opening, closing):
1174
+ return _meta_detail(meta_kind, label, text)
1175
+ if role == "user" and _AGENTS_ENVELOPE_RE.fullmatch(text or ""):
1176
+ return _meta_detail("context", "agents", text)
1177
+ return None
206
1178
 
207
1179
 
208
1180
  def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
@@ -213,14 +1185,28 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
213
1185
  if record_type == "response_item":
214
1186
  if ptype == "message":
215
1187
  role = payload.get("role")
216
- kind = "user" if role == "user" else "assistant"
217
1188
  text = _join_content_texts(payload.get("content"))
1189
+ meta = _classify_injected_message(
1190
+ role if isinstance(role, str) else None, text)
1191
+ if meta is not None:
1192
+ return _Extracted("meta", text, "text", meta, [])
1193
+ kind = "user" if role == "user" else "assistant"
1194
+ if kind == "assistant":
1195
+ clean, markers = _segment_harness_markers(text)
1196
+ if markers:
1197
+ return _Extracted(
1198
+ kind, clean, "text", {"markers": markers}, [], text)
218
1199
  return _Extracted(kind, text, "text", None, [])
219
1200
  if ptype == "reasoning":
220
1201
  summary = _join_content_texts(payload.get("summary"))
221
1202
  body = _join_content_texts(payload.get("content"))
1203
+ reasoning = _reasoning_projection(
1204
+ source="response_item", summary=summary, body=body)
1205
+ if reasoning is None:
1206
+ return None
222
1207
  text = "\n".join(p for p in (summary, body) if p)
223
- return _Extracted("reasoning", text, "search_thinking", None, [])
1208
+ return _Extracted(
1209
+ "reasoning", text, "search_thinking", {"reasoning": reasoning}, [])
224
1210
  if ptype in _RESPONSE_TOOL_CALLS:
225
1211
  name = payload.get("name") or ptype
226
1212
  if ptype == "function_call":
@@ -231,20 +1217,55 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
231
1217
  args = _stringify(payload.get("input") or payload.get("arguments"))
232
1218
  text = f"{name}\n{args}" if args else str(name)
233
1219
  detail = {"name": name, "args": args[:CODEX_TEXT_CAP]}
1220
+ card = decode_tool_call_card(payload)
1221
+ if card is None:
1222
+ card = decode_secondary_tool_call_card(payload)
1223
+ if card is not None:
1224
+ detail["card"] = card
234
1225
  return _Extracted("tool_call", text, "search_tool", detail, [])
235
1226
  if ptype in _RESPONSE_TOOL_OUTPUTS:
236
- body = _stringify(payload.get("output") or payload.get("tools"))
237
- return _Extracted("tool_output", body, "search_tool", None, [])
1227
+ value = payload["output"] if "output" in payload else payload.get("tools")
1228
+ shaped = decode_tool_output_card(payload)
1229
+ if shaped is not None:
1230
+ card, _display_body = shaped
1231
+ return _Extracted(
1232
+ "tool_output", _stringify(value), "search_tool", {"card": card}, [])
1233
+ return _Extracted("tool_output", _stringify(value), "search_tool", None, [])
238
1234
  return None # unknown response_item subtype: version tolerance
239
1235
  if record_type == "event_msg":
240
1236
  if ptype in _EVENT_PROSE_KIND:
241
1237
  kind = _EVENT_PROSE_KIND[ptype]
242
1238
  text = _stringify(payload.get("message") or payload.get("text"))
1239
+ role = "user" if kind == "user" else "assistant"
1240
+ meta = _classify_injected_message(role, text)
1241
+ if meta is not None:
1242
+ return _Extracted("meta", text, "text", meta, [])
243
1243
  column = "search_thinking" if kind == "reasoning" else "text"
1244
+ if kind == "reasoning":
1245
+ title_like = _REASONING_TITLE_RE.fullmatch(text.strip()) is not None
1246
+ reasoning = _reasoning_projection(
1247
+ source="agent_reasoning",
1248
+ summary=text if title_like else "",
1249
+ body="" if title_like else text)
1250
+ if reasoning is None:
1251
+ return None
1252
+ return _Extracted(
1253
+ kind, text, column, {"reasoning": reasoning}, [])
1254
+ if kind == "assistant":
1255
+ clean, markers = _segment_harness_markers(text)
1256
+ if markers:
1257
+ return _Extracted(
1258
+ kind, clean, column, {"markers": markers}, [], text)
244
1259
  return _Extracted(kind, text, column, None, [])
245
1260
  if ptype in _EVENT_CARD_TYPES:
246
1261
  text, touches = _event_card(ptype, payload)
247
1262
  detail = {"event": ptype}
1263
+ if ptype in {"task_started", "task_complete"}:
1264
+ detail.update(_lifecycle_projection(payload))
1265
+ patch_card = decode_patch_event_card(payload)
1266
+ card = patch_card or decode_secondary_event_card(payload)
1267
+ if card is not None:
1268
+ detail["card"] = card
248
1269
  return _Extracted("event", text, "search_tool", detail, touches)
249
1270
  return None # token_count (accounting) + unknown event types: no row
250
1271
  # session_meta / turn_context / unknown record types: no normalized row
@@ -268,8 +1289,12 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
268
1289
  text = " ".join(["patch_apply", *paths]) if paths else "patch_apply"
269
1290
  elif ptype == "mcp_tool_call_end":
270
1291
  invocation = payload.get("invocation")
271
- name = invocation.get("name") if isinstance(invocation, dict) else None
272
- text = f"mcp_tool_call {name}" if name else "mcp_tool_call"
1292
+ server = invocation.get("server") if isinstance(invocation, dict) else None
1293
+ tool = ((invocation.get("tool") or invocation.get("name"))
1294
+ if isinstance(invocation, dict) else None)
1295
+ identity = "/".join(
1296
+ value for value in (server, tool) if isinstance(value, str) and value)
1297
+ text = f"mcp_tool_call {identity}" if identity else "mcp_tool_call"
273
1298
  elif ptype == "web_search_end":
274
1299
  query = _stringify(payload.get("query"))
275
1300
  text = f"web_search {query}".strip()
@@ -278,6 +1303,75 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
278
1303
  return text, touches
279
1304
 
280
1305
 
1306
+ def infer_codex_event_turns(
1307
+ events: Iterable[Any], *, initial_turn: str | None = None
1308
+ ) -> tuple[list[str | None], str | None]:
1309
+ """Infer each physical event's logical turn from native lifecycle anchors.
1310
+
1311
+ ``turn_context``/``task_started`` establish a forward turn. A resumed
1312
+ segment can instead begin with response rows and expose its first native
1313
+ proof only on a later patch/task-complete record; in that case only the
1314
+ unanchored prefix since the latest ``session_meta`` is backfilled. Distinct
1315
+ explicit anchors are never merged.
1316
+ """
1317
+ materialized = list(events)
1318
+ turns: list[str | None] = [None] * len(materialized)
1319
+ current = initial_turn
1320
+ segment_start = 0
1321
+ for index, event in enumerate(materialized):
1322
+ record_type = getattr(event, "record_type", None)
1323
+ try:
1324
+ obj = json.loads(getattr(event, "payload_json", "") or "{}")
1325
+ except (json.JSONDecodeError, TypeError):
1326
+ obj = {}
1327
+ payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
1328
+ ptype = payload.get("type")
1329
+ if record_type == "session_meta":
1330
+ current = None
1331
+ segment_start = index + 1
1332
+ continue
1333
+ explicit = getattr(event, "turn_id", None)
1334
+ if explicit is None and record_type == "turn_context":
1335
+ candidate = payload.get("turn_id")
1336
+ explicit = candidate if isinstance(candidate, str) and candidate else None
1337
+ if explicit is not None:
1338
+ if current is None and _is_late_turn_anchor(
1339
+ record_type, ptype, explicit):
1340
+ for prior in range(segment_start, index):
1341
+ if turns[prior] is None:
1342
+ turns[prior] = explicit
1343
+ current = explicit
1344
+ turns[index] = explicit
1345
+ else:
1346
+ turns[index] = current
1347
+ return turns, current
1348
+
1349
+
1350
+ def _is_late_turn_anchor(
1351
+ record_type: str | None, payload_type: str | None, explicit_turn: str | None,
1352
+ ) -> bool:
1353
+ """Whether a native turn id can prove a preceding unanchored prefix."""
1354
+ return (
1355
+ explicit_turn is not None
1356
+ and record_type != "turn_context"
1357
+ and payload_type != "task_started"
1358
+ )
1359
+
1360
+
1361
+ def codex_event_is_late_turn_anchor(event: Any) -> bool:
1362
+ """Public predicate shared by delta persistence and turn inference."""
1363
+ try:
1364
+ obj = json.loads(getattr(event, "payload_json", "") or "{}")
1365
+ except (json.JSONDecodeError, TypeError):
1366
+ obj = {}
1367
+ payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
1368
+ return _is_late_turn_anchor(
1369
+ getattr(event, "record_type", None),
1370
+ payload.get("type"),
1371
+ getattr(event, "turn_id", None),
1372
+ )
1373
+
1374
+
281
1375
  def normalize_codex_events(
282
1376
  events: Iterable[Any], *, initial: CodexStickyState
283
1377
  ) -> CodexNormalizationResult:
@@ -285,10 +1379,13 @@ def normalize_codex_events(
285
1379
  rows + file touches, replaying sticky turn/model state seeded from ``initial``.
286
1380
 
287
1381
  Returns the terminal sticky state for persistence to codex_session_files."""
1382
+ materialized = list(events)
1383
+ inferred_turns, terminal_turn = infer_codex_event_turns(
1384
+ materialized, initial_turn=initial.turn_id)
288
1385
  sticky = CodexStickyState(turn_id=initial.turn_id, model=initial.model)
289
1386
  rows: list[CodexNormalizedRow] = []
290
1387
  touches: list[CodexFileTouch] = []
291
- for event in events:
1388
+ for event_index, event in enumerate(materialized):
292
1389
  record_type = getattr(event, "record_type", None)
293
1390
  if record_type == "session_meta":
294
1391
  sticky.turn_id = None
@@ -301,7 +1398,9 @@ def normalize_codex_events(
301
1398
  payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
302
1399
  if record_type == "turn_context":
303
1400
  own = getattr(event, "turn_id", None)
304
- sticky.turn_id = own if own is not None else payload.get("turn_id")
1401
+ candidate_turn = own if own is not None else payload.get("turn_id")
1402
+ if isinstance(candidate_turn, str) and candidate_turn:
1403
+ sticky.turn_id = candidate_turn
305
1404
  model = payload.get("model")
306
1405
  sticky.model = model if isinstance(model, str) and model else None
307
1406
  continue
@@ -313,9 +1412,10 @@ def normalize_codex_events(
313
1412
  extracted = _extract(record_type, payload)
314
1413
  if extracted is None:
315
1414
  continue
316
- own_turn = getattr(event, "turn_id", None)
317
- eff_turn = own_turn if own_turn is not None else sticky.turn_id
1415
+ eff_turn = inferred_turns[event_index]
318
1416
  text_full = extracted.content_text or ""
1417
+ identity_full = (extracted.identity_text
1418
+ if extracted.identity_text is not None else text_full)
319
1419
  capped, truncated = _cap(text_full)
320
1420
  text_col = ""
321
1421
  search_tool = ""
@@ -332,6 +1432,11 @@ def normalize_codex_events(
332
1432
  detail["truncated"] = True
333
1433
  source_path = getattr(event, "source_path", "")
334
1434
  line_offset = getattr(event, "line_offset", 0)
1435
+ effective_call_id = getattr(event, "call_id", None)
1436
+ if not effective_call_id and record_type == "response_item":
1437
+ candidate = payload.get("id")
1438
+ if isinstance(candidate, str) and candidate:
1439
+ effective_call_id = candidate
335
1440
  rows.append(CodexNormalizedRow(
336
1441
  conversation_key=conversation_key,
337
1442
  source_root_key=getattr(event, "source_root_key", None) or "",
@@ -339,14 +1444,14 @@ def normalize_codex_events(
339
1444
  line_offset=line_offset,
340
1445
  timestamp_utc=getattr(event, "timestamp_utc", None),
341
1446
  turn_id=eff_turn,
342
- call_id=getattr(event, "call_id", None),
1447
+ call_id=effective_call_id,
343
1448
  kind=extracted.kind,
344
1449
  event_type=getattr(event, "event_type", None),
345
1450
  record_family="event_msg" if record_type == "event_msg" else "response_item",
346
1451
  model=sticky.model,
347
1452
  text=text_col,
348
- content_digest=content_digest(text_full),
349
- content_len=content_len(text_full),
1453
+ content_digest=content_digest(identity_full),
1454
+ content_len=content_len(identity_full),
350
1455
  detail_json=_canonical_json(detail) if detail else None,
351
1456
  search_tool=search_tool,
352
1457
  search_thinking=search_thinking,
@@ -361,7 +1466,7 @@ def normalize_codex_events(
361
1466
  ))
362
1467
  return CodexNormalizationResult(
363
1468
  rows=rows, touches=touches,
364
- terminal=CodexStickyState(turn_id=sticky.turn_id, model=sticky.model),
1469
+ terminal=CodexStickyState(turn_id=terminal_turn, model=sticky.model),
365
1470
  )
366
1471
 
367
1472
 
@@ -383,7 +1488,7 @@ def _pair_mirrors_impl(
383
1488
  # member (three identical event copies never collapse into one).
384
1489
  turned: dict[tuple, dict[str, list[int]]] = {}
385
1490
  for i, row in enumerate(rows):
386
- if row.kind not in _PROSE_KINDS or row.turn_id is None:
1491
+ if row.kind not in _MIRROR_KINDS or row.turn_id is None:
387
1492
  continue
388
1493
  key = (row.turn_id, row.kind, row.content_digest, row.content_len)
389
1494
  group = turned.setdefault(key, {"R": [], "E": []})
@@ -399,7 +1504,7 @@ def _pair_mirrors_impl(
399
1504
  last_prose_by_kind: dict[str, int] = {}
400
1505
  paired_response: set[int] = set()
401
1506
  for j, row in enumerate(rows):
402
- if row.kind not in _PROSE_KINDS or row.turn_id is not None:
1507
+ if row.kind not in _MIRROR_KINDS or row.turn_id is not None:
403
1508
  continue
404
1509
  prev_idx = last_prose_by_kind.get(row.kind)
405
1510
  if row.record_family == "event_msg" and prev_idx is not None:
@@ -446,7 +1551,219 @@ def _item_pos(row: CodexNormalizedRow) -> tuple:
446
1551
  return (row.timestamp_utc or "", row.source_path, row.line_offset)
447
1552
 
448
1553
 
449
- def canonical_items(rows: list[CodexNormalizedRow]) -> list[dict]:
1554
+ def _row_card(row: CodexNormalizedRow) -> dict | None:
1555
+ if not row.detail_json:
1556
+ return None
1557
+ try:
1558
+ detail = json.loads(row.detail_json)
1559
+ except (json.JSONDecodeError, TypeError):
1560
+ return None
1561
+ card = detail.get("card") if isinstance(detail, dict) else None
1562
+ return card if isinstance(card, dict) else None
1563
+
1564
+
1565
+ def _fold_patch_completion_items(items: list[dict]) -> list[dict]:
1566
+ """Fold only proven ``patch_apply_end`` items into their owning response.
1567
+
1568
+ Same-id ownership wins when unique. Current nested ``tools.apply_patch``
1569
+ events use an inner id, so the second proof is a unique physical bracket in
1570
+ one source file: call < event < that call's uniquely-owned output. Ambiguous
1571
+ or repeated shapes stay separate and inspectable.
1572
+ """
1573
+ patch_calls: list[
1574
+ tuple[int, CodexNormalizedRow, CodexNormalizedRow | None, int]
1575
+ ] = []
1576
+ for item_index, item in enumerate(items):
1577
+ if item["klass"] != "response":
1578
+ continue
1579
+ owner_count: dict[str, int] = {}
1580
+ for row in item["rows"]:
1581
+ if row.kind == "tool_call" and row.call_id:
1582
+ owner_count[row.call_id] = owner_count.get(row.call_id, 0) + 1
1583
+ for row in item["rows"]:
1584
+ card = _row_card(row)
1585
+ if not (row.kind == "tool_call" and isinstance(card, dict)
1586
+ and card.get("type") == "patch"):
1587
+ continue
1588
+ outputs = [
1589
+ candidate for candidate in item["rows"]
1590
+ if candidate.kind == "tool_output" and row.call_id
1591
+ and candidate.call_id == row.call_id
1592
+ and owner_count.get(row.call_id) == 1
1593
+ and _item_pos(row) < _item_pos(candidate)
1594
+ ]
1595
+ patch_calls.append((
1596
+ item_index, row, outputs[0] if len(outputs) == 1 else None,
1597
+ owner_count.get(row.call_id, 0),
1598
+ ))
1599
+
1600
+ matched_calls: set[tuple[str, int]] = set()
1601
+ remove_items: set[int] = set()
1602
+ for event_index, event_item in enumerate(items):
1603
+ if event_item["klass"] != "event" or len(event_item["rows"]) != 1:
1604
+ continue
1605
+ event_row = event_item["rows"][0]
1606
+ event_card = _row_card(event_row)
1607
+ if not (isinstance(event_card, dict)
1608
+ and event_card.get("source") == "patch_apply_end"):
1609
+ continue
1610
+ same_id = [
1611
+ candidate for candidate in patch_calls
1612
+ if event_row.call_id and candidate[1].call_id == event_row.call_id
1613
+ and candidate[3] == 1
1614
+ and candidate[1].turn_id == event_row.turn_id
1615
+ and (candidate[1].source_path, candidate[1].line_offset) not in matched_calls
1616
+ ]
1617
+ candidates = same_id if len(same_id) == 1 else []
1618
+ if not candidates:
1619
+ candidates = [
1620
+ candidate for candidate in patch_calls
1621
+ if candidate[2] is not None
1622
+ and candidate[1].turn_id == event_row.turn_id
1623
+ and candidate[1].source_path == event_row.source_path
1624
+ and candidate[2].source_path == event_row.source_path
1625
+ and candidate[1].line_offset < event_row.line_offset < candidate[2].line_offset
1626
+ and (candidate[1].source_path, candidate[1].line_offset) not in matched_calls
1627
+ ]
1628
+ if len(candidates) != 1:
1629
+ continue
1630
+ owner_index, call_row, _output_row, _owner_count = candidates[0]
1631
+ owner = items[owner_index]
1632
+ owner["rows"].append(event_row)
1633
+ owner["rows"].sort(key=_item_pos)
1634
+ owner.setdefault("folded_items", []).append(event_item)
1635
+ matched_calls.add((call_row.source_path, call_row.line_offset))
1636
+ remove_items.add(event_index)
1637
+ return [item for index, item in enumerate(items) if index not in remove_items]
1638
+
1639
+
1640
+ def _fold_secondary_completion_items(items: list[dict]) -> list[dict]:
1641
+ """Fold web/MCP end events only onto one exact same-turn call id.
1642
+
1643
+ No name, adjacency, or basename inference is allowed: a reused, empty, or
1644
+ unmatched id leaves the native completion visible as its own event card.
1645
+ """
1646
+ calls_by_id: dict[str, list[tuple[int, CodexNormalizedRow]]] = {}
1647
+ for item_index, item in enumerate(items):
1648
+ if item["klass"] != "response":
1649
+ continue
1650
+ for row in item["rows"]:
1651
+ if row.kind == "tool_call" and row.call_id:
1652
+ calls_by_id.setdefault(row.call_id, []).append((item_index, row))
1653
+ remove_items: set[int] = set()
1654
+ matched_calls: set[tuple[str, int]] = set()
1655
+ for event_index, event_item in enumerate(items):
1656
+ if event_item["klass"] != "event" or len(event_item["rows"]) != 1:
1657
+ continue
1658
+ event_row = event_item["rows"][0]
1659
+ event_card = _row_card(event_row)
1660
+ if not (event_row.call_id and isinstance(event_card, dict)
1661
+ and event_card.get("type") in {
1662
+ "web_search_completion", "mcp_completion"}):
1663
+ continue
1664
+ candidates = [
1665
+ (owner_index, call_row)
1666
+ for owner_index, call_row in calls_by_id.get(event_row.call_id, [])
1667
+ if call_row.turn_id == event_row.turn_id
1668
+ and (call_row.source_path, call_row.line_offset) not in matched_calls
1669
+ ]
1670
+ if event_card.get("type") == "web_search_completion":
1671
+ candidates = [
1672
+ candidate for candidate in candidates
1673
+ if (_parse_row_detail(candidate[1]) or {}).get("name")
1674
+ == "web_search_call"
1675
+ ]
1676
+ if len(candidates) != 1:
1677
+ continue
1678
+ owner_index, call_row = candidates[0]
1679
+ owner = items[owner_index]
1680
+ owner["rows"].append(event_row)
1681
+ owner["rows"].sort(key=_item_pos)
1682
+ owner.setdefault("folded_items", []).append(event_item)
1683
+ matched_calls.add((call_row.source_path, call_row.line_offset))
1684
+ remove_items.add(event_index)
1685
+ return [item for index, item in enumerate(items) if index not in remove_items]
1686
+
1687
+
1688
+ def _parse_row_detail(row: CodexNormalizedRow) -> dict | None:
1689
+ if not row.detail_json:
1690
+ return None
1691
+ try:
1692
+ detail = json.loads(row.detail_json)
1693
+ except (json.JSONDecodeError, TypeError):
1694
+ return None
1695
+ return detail if isinstance(detail, dict) else None
1696
+
1697
+
1698
+ def _fold_lifecycle_items(items: list[dict]) -> list[dict]:
1699
+ """Fold only closed, uniquely-owned redundant task lifecycle events.
1700
+
1701
+ The physical rows remain retained and become member-key aliases. A completion
1702
+ is redundant only when its last message is empty or digest/length-identical to
1703
+ a real assistant row in the one owning logical response. Unknown fields,
1704
+ errors, unmatched/ambiguous events, and unique messages stay standalone.
1705
+ """
1706
+ responses: dict[str, list[tuple[int, dict]]] = {}
1707
+ events: dict[tuple[str, str], list[tuple[int, dict, CodexNormalizedRow, dict]]] = {}
1708
+ for index, item in enumerate(items):
1709
+ turn_id = item.get("turn_id")
1710
+ if item["klass"] == "response" and turn_id:
1711
+ responses.setdefault(turn_id, []).append((index, item))
1712
+ if item["klass"] != "event" or len(item["rows"]) != 1 or not turn_id:
1713
+ continue
1714
+ row = item["rows"][0]
1715
+ detail = _parse_row_detail(row)
1716
+ lifecycle = detail.get("lifecycle") if isinstance(detail, dict) else None
1717
+ if (isinstance(lifecycle, dict)
1718
+ and lifecycle.get("event") in {"task_started", "task_complete"}):
1719
+ events.setdefault((turn_id, lifecycle["event"]), []).append(
1720
+ (index, item, row, detail))
1721
+
1722
+ remove: set[int] = set()
1723
+ for turn_id, owners in responses.items():
1724
+ if len(owners) != 1:
1725
+ continue
1726
+ _owner_index, owner = owners[0]
1727
+ for event_name in ("task_started", "task_complete"):
1728
+ candidates = events.get((turn_id, event_name), [])
1729
+ if len(candidates) != 1:
1730
+ continue
1731
+ event_index, event_item, row, detail = candidates[0]
1732
+ if detail.get("_lifecycle_foldable") is not True:
1733
+ continue
1734
+ if event_name == "task_complete":
1735
+ message_len = detail.get("_lifecycle_message_len")
1736
+ message_digest = detail.get("_lifecycle_message_digest")
1737
+ if message_len:
1738
+ if not any(
1739
+ candidate.kind == "assistant"
1740
+ and candidate.content_len == message_len
1741
+ and candidate.content_digest == message_digest
1742
+ for candidate in owner["rows"]
1743
+ ):
1744
+ continue
1745
+ lifecycle = detail["lifecycle"]
1746
+ section = {
1747
+ key: value for key, value in lifecycle.items()
1748
+ if key not in {"schema_version", "event", "state", "message", "error"}
1749
+ }
1750
+ folded = owner.setdefault(
1751
+ "lifecycle", {"schema_version": 1, "state": "started"})
1752
+ folded["started" if event_name == "task_started" else "completed"] = section
1753
+ if event_name == "task_complete":
1754
+ folded["state"] = "completed"
1755
+ owner["rows"].append(row)
1756
+ owner["rows"].sort(key=_item_pos)
1757
+ owner.setdefault("lifecycle_rows", []).append(row)
1758
+ owner["lifecycle_rows"].sort(key=_item_pos)
1759
+ owner.setdefault("folded_items", []).append(event_item)
1760
+ remove.add(event_index)
1761
+ return [item for index, item in enumerate(items) if index not in remove]
1762
+
1763
+
1764
+ def canonical_items(
1765
+ rows: list[CodexNormalizedRow], *, fold_patch_completions: bool = True,
1766
+ ) -> list[dict]:
450
1767
  """Group KEPT (post-pairing) rows into canonical rendered items (§5.2).
451
1768
 
452
1769
  Response items bundle a turn's assistant/reasoning/tool rows; prompt/event/
@@ -460,6 +1777,9 @@ def canonical_items(rows: list[CodexNormalizedRow]) -> list[dict]:
460
1777
  if row.turn_id is None:
461
1778
  items.append({"klass": "unturned", "rows": [row], "turn_id": None,
462
1779
  "anchor_row": row})
1780
+ elif row.kind == "meta":
1781
+ items.append({"klass": "meta", "rows": [row], "turn_id": row.turn_id,
1782
+ "anchor_row": row})
463
1783
  elif row.kind == "user":
464
1784
  items.append({"klass": "prompt", "rows": [row], "turn_id": row.turn_id,
465
1785
  "anchor_row": row})
@@ -474,6 +1794,10 @@ def canonical_items(rows: list[CodexNormalizedRow]) -> list[dict]:
474
1794
  response_by_turn[row.turn_id] = item
475
1795
  items.append(item)
476
1796
  item["rows"].append(row)
1797
+ if fold_patch_completions:
1798
+ items = _fold_patch_completion_items(items)
1799
+ items = _fold_secondary_completion_items(items)
1800
+ items = _fold_lifecycle_items(items)
477
1801
  items.sort(key=lambda it: _item_pos(it["anchor_row"]))
478
1802
  return items
479
1803