cctally 1.91.0 → 1.92.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/_cctally_cache.py +863 -74
  3. package/bin/_cctally_config.py +57 -0
  4. package/bin/_cctally_core.py +39 -8
  5. package/bin/_cctally_dashboard.py +146 -5
  6. package/bin/_cctally_dashboard_conversation.py +164 -18
  7. package/bin/_cctally_dashboard_envelope.py +2 -0
  8. package/bin/_cctally_db.py +372 -10
  9. package/bin/_cctally_doctor.py +18 -1
  10. package/bin/_cctally_journal.py +535 -13
  11. package/bin/_cctally_journal_repair.py +6 -0
  12. package/bin/_cctally_parser.py +6 -0
  13. package/bin/_cctally_quota.py +171 -55
  14. package/bin/_cctally_record.py +13 -1
  15. package/bin/_cctally_rederive.py +4 -0
  16. package/bin/_cctally_store.py +311 -6
  17. package/bin/_cctally_transcript.py +32 -2
  18. package/bin/_lib_cache_report.py +8 -3
  19. package/bin/_lib_codex_conversation.py +851 -81
  20. package/bin/_lib_codex_conversation_query.py +2005 -95
  21. package/bin/_lib_codex_find_projection.py +370 -0
  22. package/bin/_lib_codex_harness_preamble.py +176 -0
  23. package/bin/_lib_codex_hooks.py +5 -3
  24. package/bin/_lib_codex_js_scan.py +254 -0
  25. package/bin/_lib_codex_landmarks.py +309 -0
  26. package/bin/_lib_codex_title_clean.py +116 -0
  27. package/bin/_lib_conversation_dispatch.py +153 -21
  28. package/bin/_lib_conversation_watch.py +4 -2
  29. package/bin/_lib_doctor.py +64 -0
  30. package/bin/_lib_quota_alert_axes.py +31 -34
  31. package/bin/_lib_stats_damage.py +523 -0
  32. package/bin/cctally +5 -0
  33. package/dashboard/static/assets/index-BEzzJtUd.js +97 -0
  34. package/dashboard/static/assets/{index-Dwirao3Y.css → index-DnWdv8um.css} +1 -1
  35. package/dashboard/static/dashboard.html +2 -2
  36. package/package.json +7 -1
  37. package/dashboard/static/assets/index-CILAoEja.js +0 -90
@@ -25,6 +25,8 @@ from typing import Any, Iterable
25
25
  # _lib_conversation_query. These modules are stdlib-only pure kernels.
26
26
  from _lib_conversation import _strip_ansi
27
27
  from _lib_conversation_query import _TITLE_MAX, _first_nonblank_line
28
+ from _lib_codex_harness_preamble import parse_harness_preamble
29
+ from _lib_codex_js_scan import scan_tool_invocations
28
30
 
29
31
 
30
32
  # ── digest contract (§3.1) ────────────────────────────────────────────────────
@@ -252,6 +254,17 @@ _EXEC_METADATA_KEYS = frozenset({
252
254
  "justification", "login", "max_output_tokens", "prefix_rule",
253
255
  "sandbox_permissions", "shell", "tty", "yield_time_ms",
254
256
  })
257
+ # The FROZEN v1 ingest projection, kept deliberately rather than deleted.
258
+ #
259
+ # Live decoding moved to `_lib_codex_harness_preamble.parse_harness_preamble`
260
+ # (#463 S3, spec section 4.1), which this regex could never do: it requires a
261
+ # blank line before `Output:` and end-of-string after it, and 0 of the 39,942
262
+ # production outputs carrying the preamble it targets match it. But `_extract`
263
+ # must keep storing byte-identical `detail_json` (spec section 3.0), because
264
+ # `_row_source_bytes` feeds `PAGE_SOURCE_BYTE_BUDGET` and a richer stored card
265
+ # would move page boundaries for newly ingested rows only. So this stays as the
266
+ # stored contract until `CODEX_CONVERSATION_CONTRACT_VERSION` is bumped and the
267
+ # history is replayed. It is reached ONLY through `for_storage=True`.
255
268
  _HARNESS_STATUS_RE = re.compile(
256
269
  r"\A(Script completed|Script failed)\nWall time ([^\n]+)\n\nOutput:\Z")
257
270
 
@@ -451,14 +464,85 @@ def _json_object(value: Any) -> dict | None:
451
464
  return parsed if isinstance(parsed, dict) else None
452
465
 
453
466
 
467
+ def _decode_shell_family_card(
468
+ payload: dict, name: Any, budget: _TextBudget, text_cap: int,
469
+ ) -> dict | None:
470
+ """The four uncarded `function_call` families (spec section 3.2).
471
+
472
+ `exec_command` maps onto the EXISTING `terminal` card rather than a new type,
473
+ so `BashCard` renders it unchanged. `write_stdin` and `wait` produce
474
+ `session_ref`; grouping happens only at `shell` scope, and a cell reference
475
+ is never presented as a shell session. `js` produces the `program` card of
476
+ section 3.3 carrying its authored `title`, which is the only authored
477
+ description of a program available anywhere in the payload.
478
+ """
479
+ if name == "exec_command":
480
+ arguments = _json_object(payload.get("arguments"))
481
+ command = _command_invocation(arguments, budget)
482
+ if command is None:
483
+ return None
484
+ card = {
485
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
486
+ "type": "terminal",
487
+ "status": (payload.get("status")
488
+ if isinstance(payload.get("status"), str) else "unknown"),
489
+ "commands": [command],
490
+ }
491
+ if budget.truncated:
492
+ card["truncated"] = True
493
+ return card
494
+ if name in _SESSION_TOOLS:
495
+ arguments = _json_object(payload.get("arguments"))
496
+ session = _session_invocation(name, arguments, budget)
497
+ if session is None:
498
+ return None
499
+ return {
500
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
501
+ "type": "session_ref", **session, "truncated": budget.truncated,
502
+ }
503
+ if name == "js":
504
+ arguments = _json_object(payload.get("arguments"))
505
+ if arguments is None:
506
+ return None
507
+ title = arguments.get("title")
508
+ if title is not None and not isinstance(title, str):
509
+ return None
510
+ return _program_card(
511
+ arguments.get("code"),
512
+ title=budget.take(title) if isinstance(title, str) else None,
513
+ text_cap=text_cap)
514
+ if name == "tool_search_call":
515
+ arguments = _json_object(payload.get("arguments"))
516
+ if arguments is None or not isinstance(arguments.get("query"), str):
517
+ return None
518
+ limit = arguments.get("limit")
519
+ if limit is not None and (not isinstance(limit, int) or isinstance(limit, bool)):
520
+ return None
521
+ card = {
522
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
523
+ "type": "tool_search", "query": budget.take(arguments["query"]),
524
+ "limit": limit,
525
+ }
526
+ if budget.truncated:
527
+ card["truncated"] = True
528
+ return card
529
+ return None
530
+
531
+
454
532
  def decode_secondary_tool_call_card(
455
- payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
533
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP, for_storage: bool = False,
456
534
  ) -> dict | None:
457
- """Bounded additive wire for plans, web actions, and agent operations.
535
+ """Bounded additive wire for plans, web actions, agent operations, and the
536
+ shell/program families #463 S3 adds.
458
537
 
459
538
  Unknown or malformed shapes deliberately return ``None`` so the existing
460
539
  provider-name + raw-argument fallback remains visible and payload readback
461
540
  stays authoritative.
541
+
542
+ ``for_storage=True`` produces the FROZEN v1 projection ``_extract`` persists
543
+ (spec section 3.0), which means the five S3 families decode to nothing: they
544
+ reach 34,935 rows, more than any other S3 change, and persisting them would
545
+ move `PAGE_SOURCE_BYTE_BUDGET` boundaries for newly ingested rows only.
462
546
  """
463
547
  if not isinstance(payload, dict):
464
548
  return None
@@ -466,6 +550,10 @@ def decode_secondary_tool_call_card(
466
550
  name = payload.get("name")
467
551
  status = payload.get("status") if isinstance(payload.get("status"), str) else "requested"
468
552
  budget = _TextBudget(text_cap)
553
+ if not for_storage and ptype == "function_call":
554
+ s3_card = _decode_shell_family_card(payload, name, budget, text_cap)
555
+ if s3_card is not None:
556
+ return s3_card
469
557
  if ptype == "function_call" and name == "update_plan":
470
558
  arguments = _json_object(payload.get("arguments"))
471
559
  plan = arguments.get("plan") if isinstance(arguments, dict) else None
@@ -590,7 +678,12 @@ def decode_secondary_event_card(
590
678
  return None
591
679
  result = payload.get("result")
592
680
  if isinstance(result, dict) and "Ok" in result:
593
- status = "ok"
681
+ ok_result = result.get("Ok")
682
+ status = (
683
+ "error"
684
+ if isinstance(ok_result, dict) and ok_result.get("isError") is True
685
+ else "ok"
686
+ )
594
687
  elif isinstance(result, dict) and any(key in result for key in ("Err", "Error")):
595
688
  status = "error"
596
689
  else:
@@ -612,8 +705,77 @@ def decode_secondary_event_card(
612
705
  return None
613
706
 
614
707
 
615
- def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
616
- """Decode only the complete current exec harness statement grammar."""
708
+ # The two tools that name a session rather than run a command, and what each
709
+ # names. `session_id` and `cell_id` are NOT two spellings of one thing: the
710
+ # namespaces have zero overlapping values, and a `cell_id` is a per-conversation
711
+ # sandbox-cell ordinal. `wait` polls a cell; it does not poll a shell session.
712
+ _SESSION_TOOLS = {
713
+ "write_stdin": ("shell", "write"),
714
+ "wait": ("cell", "poll"),
715
+ }
716
+
717
+ # `_exec_chain` outcomes. The distinction matters: a source that is not the
718
+ # strict chain grammar at all may still be a legible PROGRAM, while one that has
719
+ # the chain's shape and was refused by the literal parser or the command cap must
720
+ # keep producing no card, which is what the shipped closed-parser regression
721
+ # requires for `process.env.SECRET`, a template argument and nine invocations.
722
+ _EXEC_OK = "ok"
723
+ _EXEC_SHAPE = "shape"
724
+ _EXEC_REFUSED = "refused"
725
+
726
+
727
+ def _command_invocation(value: Any, budget: _TextBudget) -> dict | None:
728
+ """One `exec_command` argument object as the shared `terminal` command."""
729
+ if not isinstance(value, dict) or not isinstance(value.get("cmd"), str):
730
+ return None
731
+ workdir = value.get("workdir")
732
+ if workdir is not None and not isinstance(workdir, str):
733
+ return None
734
+ command = {
735
+ "workdir": budget.take(workdir) if isinstance(workdir, str) else None,
736
+ "command": "",
737
+ "metadata": {},
738
+ }
739
+ command["command"] = budget.take(value["cmd"])
740
+ for key in sorted(_EXEC_METADATA_KEYS):
741
+ if key not in value:
742
+ continue
743
+ bounded = _bounded_metadata(value[key], budget)
744
+ if bounded is not None:
745
+ command["metadata"][key] = bounded
746
+ return command
747
+
748
+
749
+ def _session_invocation(name: str, value: Any, budget: _TextBudget) -> dict | None:
750
+ """One `write_stdin`/`wait` argument object as a session reference.
751
+
752
+ ``ref`` carries the provider's own identifier here, because the kernel has no
753
+ conversation context. The query layer replaces a ``shell`` ref with the
754
+ conversation-local ordinal before publishing, so no raw provider session id
755
+ ever reaches a reader (spec section 4.3).
756
+ """
757
+ if not isinstance(value, dict):
758
+ return None
759
+ scope, operation = _SESSION_TOOLS[name]
760
+ raw = value.get("session_id") if scope == "shell" else value.get("cell_id")
761
+ if isinstance(raw, bool) or not isinstance(raw, (str, int)):
762
+ return None
763
+ chars = value.get("chars")
764
+ if chars is not None and not isinstance(chars, str):
765
+ return None
766
+ return {
767
+ "scope": scope, "ref": budget.take(str(raw)), "operation": operation,
768
+ "chars": budget.take(chars) if isinstance(chars, str) else None,
769
+ }
770
+
771
+
772
+ def _exec_chain(source: str, *, budget: _TextBudget) -> tuple[str, list[dict] | None]:
773
+ """Decode only the complete current exec harness statement grammar.
774
+
775
+ Returns ``(_EXEC_OK, commands)``, ``(_EXEC_SHAPE, None)`` when the source is
776
+ not that grammar, or ``(_EXEC_REFUSED, None)`` when it HAS the grammar's
777
+ shape but a literal or the command cap refused it.
778
+ """
617
779
  commands: list[dict] = []
618
780
  pos = 0
619
781
  while True:
@@ -622,8 +784,10 @@ def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
622
784
  r"await\s+tools\.exec_command",
623
785
  source[pos:],
624
786
  )
625
- if prefix is None or len(commands) >= _CARD_MAX_COMMANDS:
626
- return None
787
+ if prefix is None:
788
+ return _EXEC_SHAPE, None
789
+ if len(commands) >= _CARD_MAX_COMMANDS:
790
+ return _EXEC_REFUSED, None
627
791
  variable = prefix.group(1)
628
792
  parser = _HarnessLiteralParser(source, pos + prefix.end())
629
793
  try:
@@ -631,24 +795,10 @@ def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
631
795
  value = parser.value()
632
796
  parser._consume(")")
633
797
  except _LiteralError:
634
- return None
635
- if not isinstance(value, dict) or not isinstance(value.get("cmd"), str):
636
- return None
637
- workdir = value.get("workdir")
638
- if workdir is not None and not isinstance(workdir, str):
639
- return None
640
- command = {
641
- "workdir": budget.take(workdir) if isinstance(workdir, str) else None,
642
- "command": "",
643
- "metadata": {},
644
- }
645
- command["command"] = budget.take(value["cmd"])
646
- for key in sorted(_EXEC_METADATA_KEYS):
647
- if key not in value:
648
- continue
649
- bounded = _bounded_metadata(value[key], budget)
650
- if bounded is not None:
651
- command["metadata"][key] = bounded
798
+ return _EXEC_REFUSED, None
799
+ command = _command_invocation(value, budget)
800
+ if command is None:
801
+ return _EXEC_REFUSED, None
652
802
  commands.append(command)
653
803
  suffix = re.match(
654
804
  r"\s*;\s*text\s*\(\s*" + re.escape(variable)
@@ -656,10 +806,92 @@ def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
656
806
  source[parser.pos:],
657
807
  )
658
808
  if suffix is None:
659
- return None
809
+ return _EXEC_SHAPE, None
660
810
  pos = parser.pos + suffix.end()
661
811
  if not source[pos:].strip():
662
- return commands
812
+ return _EXEC_OK, commands
813
+
814
+
815
+ def _scanned_invocations(
816
+ source: str, *, budget: _TextBudget,
817
+ ) -> tuple[list[dict], bool] | None:
818
+ """``(invocations, over_cap)`` for a program, or ``None`` if it cannot be lexed.
819
+
820
+ Arguments are read by the existing ``_HarnessLiteralParser``, so a non-literal
821
+ such as ``process.env.SECRET`` is still refused — the entry degrades to
822
+ ``other``, which names the tool and claims nothing about what it was given.
823
+ """
824
+ located = scan_tool_invocations(source, limit=_CARD_MAX_COMMANDS + 1)
825
+ if located is None:
826
+ return None
827
+ invocations: list[dict] = []
828
+ for name, paren in located[:_CARD_MAX_COMMANDS]:
829
+ parser = _HarnessLiteralParser(source, paren)
830
+ try:
831
+ parser._consume("(")
832
+ value = parser.value()
833
+ except _LiteralError:
834
+ value = None
835
+ entry: dict | None = None
836
+ if name == "exec_command":
837
+ command = _command_invocation(value, budget)
838
+ if command is not None:
839
+ entry = {"kind": "command", **command}
840
+ elif name in _SESSION_TOOLS:
841
+ session = _session_invocation(name, value, budget)
842
+ if session is not None:
843
+ entry = {"kind": "session", **session}
844
+ if entry is None:
845
+ entry = {"kind": "other", "name": budget.take(name)}
846
+ invocations.append(entry)
847
+ return invocations, len(located) > _CARD_MAX_COMMANDS
848
+
849
+
850
+ def _program_card_from_scan(
851
+ source: str, *, title: str | None, budget: _TextBudget,
852
+ ) -> dict | None:
853
+ """A `program` card for a source the strict chain grammar did not match."""
854
+ scanned = _scanned_invocations(source, budget=budget)
855
+ if scanned is None:
856
+ return None
857
+ invocations, over_cap = scanned
858
+ if not invocations:
859
+ return None
860
+ return {
861
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
862
+ "type": "program", "title": title,
863
+ # Never claim the program did only what is listed: the scanner models
864
+ # `tools.<name>(` and nothing else, so anything reaching here contains
865
+ # statements it did not read.
866
+ "complete": False,
867
+ "invocations": invocations,
868
+ "truncated": over_cap or budget.truncated,
869
+ }
870
+
871
+
872
+ def _program_card(
873
+ source: Any, *, title: str | None, text_cap: int,
874
+ ) -> dict | None:
875
+ """A `program` card for an arbitrary authored program body.
876
+
877
+ A body that IS the complete recognized chain is still a program card — the
878
+ `js` family always renders as one — but it is marked `complete`.
879
+ """
880
+ if not isinstance(source, str) or len(source) > _CARD_HARNESS_PARSE_CAP:
881
+ return None
882
+ budget = _TextBudget(text_cap)
883
+ outcome, commands = _exec_chain(source, budget=budget)
884
+ if outcome == _EXEC_REFUSED:
885
+ return None
886
+ if outcome == _EXEC_OK:
887
+ return {
888
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
889
+ "type": "program", "title": title, "complete": True,
890
+ "invocations": [{"kind": "command", **command} for command in commands],
891
+ "truncated": budget.truncated,
892
+ }
893
+ return _program_card_from_scan(
894
+ source, title=title, budget=_TextBudget(text_cap))
663
895
 
664
896
 
665
897
  def _decode_apply_patch_program(source: str) -> str | None:
@@ -694,23 +926,141 @@ def _apply_patch_heredoc(command: str) -> str | None:
694
926
  return None
695
927
 
696
928
 
929
+ _APPLY_PATCH_ACTION_RE = re.compile(r"\*\*\* (Add|Update|Delete) File: (.+)\Z")
930
+ _APPLY_PATCH_MOVE_RE = re.compile(r"\*\*\* Move to: (.+)\Z")
931
+
932
+
933
+ def _apply_patch_row_kind(line: str) -> str:
934
+ """Classify one V4A body row the way the client's diff parser does."""
935
+ head = line[:1]
936
+ if head == "+":
937
+ return "add"
938
+ if head == "-":
939
+ return "del"
940
+ if head == "\\":
941
+ return "marker"
942
+ return "context"
943
+
944
+
945
+ def _apply_patch_unified_diff(
946
+ old_path: str, new_path: str, body: list[str],
947
+ ) -> str | None:
948
+ """One section of a proven ``apply_patch`` envelope as a unified diff.
949
+
950
+ The V4A envelope states no line offsets — its ``@@`` markers carry context
951
+ TEXT, not numbers — so each hunk header counts the rows it contains and
952
+ numbers them relative to the file's first hunk. That is the same relative
953
+ numbering the edit-diff card has always rendered under, and the running
954
+ counts continue across a file's hunks so two hunks never show one gutter
955
+ number twice.
956
+
957
+ A section with no body row at all — a V4A ``*** Delete File:`` names the
958
+ file and carries nothing — yields ``None``, so the card claims no diff
959
+ rather than publishing an empty one.
960
+ """
961
+ hunks: list[list[str]] = []
962
+ current: list[str] | None = None
963
+ for line in body:
964
+ if line.startswith("@@"):
965
+ current = []
966
+ hunks.append(current)
967
+ continue
968
+ if current is None:
969
+ current = []
970
+ hunks.append(current)
971
+ current.append(line)
972
+ hunks = [hunk for hunk in hunks if hunk]
973
+ if not hunks:
974
+ return None
975
+ out = [f"--- {old_path}\n", f"+++ {new_path}\n"]
976
+ old_at = new_at = 1
977
+ for hunk in hunks:
978
+ kinds = [_apply_patch_row_kind(line) for line in hunk]
979
+ old_count = sum(1 for kind in kinds if kind in ("context", "del"))
980
+ new_count = sum(1 for kind in kinds if kind in ("context", "add"))
981
+ out.append(
982
+ f"@@ -{old_at if old_count else 0},{old_count} "
983
+ f"+{new_at if new_count else 0},{new_count} @@\n")
984
+ out.extend(f"{line}\n" for line in hunk)
985
+ old_at += old_count
986
+ new_at += new_count
987
+ return "".join(out)
988
+
989
+
697
990
  def _patch_files_from_apply_patch(
698
- patch: str, budget: _TextBudget | None = None,
991
+ patch: str, budget: _TextBudget | None = None, *, with_diffs: bool = False,
699
992
  ) -> list[dict]:
993
+ """The file entries of a proven ``apply_patch`` envelope.
994
+
995
+ ``with_diffs`` is READ TIME only. The stored v1 projection is the file LIST
996
+ it has always been, because `detail_json` bytes feed `_row_source_bytes` and
997
+ therefore page boundaries; publishing the diffs there would paginate a newly
998
+ ingested conversation differently from an identical historical one.
999
+
1000
+ At read time the entries gain the same field vocabulary the
1001
+ ``patch_apply_end`` card publishes — ``unified_diff``, ``diff_source`` and
1002
+ per-file ``truncated`` — through the same `_allocate_diff_budget`
1003
+ allocation, so one patch does not read two ways depending on which side of
1004
+ it the reader is looking at.
1005
+ """
700
1006
  files: list[dict] = []
1007
+ raw_paths: list[str] = []
1008
+ bodies: list[list[str]] = []
1009
+ # Past the file cap a section is dropped whole, so its body must not be
1010
+ # attributed to the previous file's diff.
1011
+ collecting = False
701
1012
  for line in patch.splitlines():
702
- match = re.match(r"\*\*\* (Add|Update|Delete) File: (.+)\Z", line)
703
- if match is not None and len(files) < _CARD_MAX_FILES:
704
- status = {"Add": "added", "Update": "modified", "Delete": "deleted"}[
705
- match.group(1)]
706
- path = budget.take(match.group(2)) if budget is not None else match.group(2)
707
- files.append({"path": path, "status": status})
1013
+ match = _APPLY_PATCH_ACTION_RE.match(line)
1014
+ if match is not None:
1015
+ collecting = len(files) < _CARD_MAX_FILES
1016
+ if collecting:
1017
+ status = {"Add": "added", "Update": "modified", "Delete": "deleted"}[
1018
+ match.group(1)]
1019
+ path = (budget.take(match.group(2)) if budget is not None
1020
+ else match.group(2))
1021
+ files.append({"path": path, "status": status})
1022
+ raw_paths.append(match.group(2))
1023
+ bodies.append([])
708
1024
  continue
709
- move = re.match(r"\*\*\* Move to: (.+)\Z", line)
710
- if move is not None and files:
1025
+ move = _APPLY_PATCH_MOVE_RE.match(line)
1026
+ if move is not None and collecting and files:
711
1027
  files[-1]["move_path"] = (
712
1028
  budget.take(move.group(1)) if budget is not None else move.group(1))
713
1029
  files[-1]["status"] = "moved"
1030
+ continue
1031
+ if line.startswith("*** "):
1032
+ continue
1033
+ if collecting and bodies:
1034
+ bodies[-1].append(line)
1035
+ if not with_diffs:
1036
+ return files
1037
+ diffs: list[str | None] = []
1038
+ for entry, raw_path, body in zip(files, raw_paths, bodies):
1039
+ # The header names the PROVIDER's own path, which can differ from the
1040
+ # entry's when the budget clipped the latter — the same rule the
1041
+ # synthesized event-side diff follows.
1042
+ move_path = entry.get("move_path")
1043
+ if entry["status"] == "added":
1044
+ old_path, new_path = "/dev/null", raw_path
1045
+ elif entry["status"] == "deleted":
1046
+ old_path, new_path = raw_path, "/dev/null"
1047
+ else:
1048
+ old_path, new_path = raw_path, move_path or raw_path
1049
+ diff = _apply_patch_unified_diff(old_path, new_path, body)
1050
+ # `truncated` on EVERY entry, so a client never has to read an absent
1051
+ # key as false; `_allocate_diff_budget` overwrites it where a diff runs.
1052
+ entry["truncated"] = False
1053
+ if diff is not None:
1054
+ # The provider transmitted a patch envelope, not a unified diff, so
1055
+ # this is a rendering of retained content — never `retained`.
1056
+ entry["diff_source"] = "derived"
1057
+ diffs.append(diff)
1058
+ if budget is not None:
1059
+ _allocate_diff_budget(files, diffs, budget)
1060
+ else:
1061
+ for entry, diff in zip(files, diffs):
1062
+ if diff is not None:
1063
+ entry["unified_diff"] = diff
714
1064
  return files
715
1065
 
716
1066
 
@@ -726,8 +1076,16 @@ def _complete_apply_patch(patch: str) -> bool:
726
1076
  for line in lines[1:-1])
727
1077
 
728
1078
 
729
- def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
730
- """Return the additive card contract for a structurally proven call."""
1079
+ def decode_tool_call_card(
1080
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP, for_storage: bool = False,
1081
+ ) -> dict | None:
1082
+ """Return the additive card contract for a structurally proven call.
1083
+
1084
+ ``for_storage=True`` produces the FROZEN v1 projection ``_extract`` persists
1085
+ (spec section 3.0): the `program` card is a read-time addition and is not
1086
+ stored, because it would newly appear on 17,777 rows and move
1087
+ `PAGE_SOURCE_BYTE_BUDGET` boundaries for whichever binary ingested them.
1088
+ """
731
1089
  if not isinstance(payload, dict) or payload.get("type") != "custom_tool_call":
732
1090
  return None
733
1091
  name = payload.get("name")
@@ -739,7 +1097,11 @@ def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> d
739
1097
  budget = _TextBudget(text_cap)
740
1098
  status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
741
1099
  if name == "apply_patch" and _complete_apply_patch(value):
742
- files = _patch_files_from_apply_patch(value, budget)
1100
+ # The derived diffs are taken from the shared budget BEFORE the raw
1101
+ # `patch`: `patch` is the payload disclosure's copy and no card renders
1102
+ # it, while the diffs are the only thing on this card a reader reads.
1103
+ files = _patch_files_from_apply_patch(
1104
+ value, budget, with_diffs=not for_storage)
743
1105
  patch = budget.take(value)
744
1106
  return {
745
1107
  "schema_version": CODEX_CARD_SCHEMA_VERSION,
@@ -751,7 +1113,8 @@ def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> d
751
1113
  return None
752
1114
  patch_value = _decode_apply_patch_program(value)
753
1115
  if patch_value is not None and _complete_apply_patch(patch_value):
754
- files = _patch_files_from_apply_patch(patch_value, budget)
1116
+ files = _patch_files_from_apply_patch(
1117
+ patch_value, budget, with_diffs=not for_storage)
755
1118
  patch = budget.take(patch_value)
756
1119
  return {
757
1120
  "schema_version": CODEX_CARD_SCHEMA_VERSION,
@@ -759,13 +1122,26 @@ def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> d
759
1122
  "patch": patch, "files": files,
760
1123
  "truncated": budget.truncated,
761
1124
  }
762
- commands = _exec_invocations(value, budget=budget)
763
- if commands is None:
1125
+ outcome, commands = _exec_chain(value, budget=budget)
1126
+ if outcome == _EXEC_REFUSED:
1127
+ # The chain's SHAPE with a literal or the command cap refusing it. This
1128
+ # is the arm the shipped closed-parser regression pins: a
1129
+ # `process.env.SECRET` argument, a template argument and nine
1130
+ # invocations must all keep producing no card at all.
764
1131
  return None
1132
+ if outcome == _EXEC_SHAPE:
1133
+ # Not the chain grammar — but 17,777 uncarded `exec` calls are programs
1134
+ # that declare constants, filter collections and invoke two different
1135
+ # tool families, and one `terminal` card cannot express that.
1136
+ if for_storage:
1137
+ return None
1138
+ return _program_card_from_scan(
1139
+ value, title=None, budget=_TextBudget(text_cap))
765
1140
  if len(commands) == 1:
766
1141
  heredoc = _apply_patch_heredoc(commands[0]["command"])
767
1142
  if heredoc is not None and _complete_apply_patch(heredoc):
768
- files = _patch_files_from_apply_patch(heredoc, budget)
1143
+ files = _patch_files_from_apply_patch(
1144
+ heredoc, budget, with_diffs=not for_storage)
769
1145
  return {
770
1146
  "schema_version": CODEX_CARD_SCHEMA_VERSION,
771
1147
  "type": "patch", "source": "exec_apply_patch", "status": status,
@@ -781,10 +1157,33 @@ def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> d
781
1157
  return card
782
1158
 
783
1159
 
1160
+ def _output_head_text(value: Any) -> str | None:
1161
+ """The text of an output's FIRST part, for the two shapes that carry one.
1162
+
1163
+ Both the array and the bare-string envelope occur for both record types, and
1164
+ the preamble is positionally guaranteed to be at the head of whichever one
1165
+ arrives, so this is the only place it may be looked for (spec section 4.1).
1166
+ """
1167
+ if isinstance(value, str):
1168
+ return value
1169
+ if (isinstance(value, dict) and value.get("type") == "input_text"
1170
+ and isinstance(value.get("text"), str)):
1171
+ return value["text"]
1172
+ return None
1173
+
1174
+
784
1175
  def decode_tool_output_card(
785
- payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
1176
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP, for_storage: bool = False,
786
1177
  ) -> tuple[dict, str] | None:
787
- """Unwrap supported output envelopes without losing malformed parts."""
1178
+ """Unwrap supported output envelopes without losing malformed parts.
1179
+
1180
+ ``for_storage=True`` produces the FROZEN v1 projection ``_extract`` persists
1181
+ (spec section 3.0): no ``exit_code``, no ``wall_time_seconds``, the status
1182
+ resolved only by ``_HARNESS_STATUS_RE``, and the preamble left in the part.
1183
+ Every reader path takes the default, which resolves the status from the five
1184
+ real grammars and removes the consumed lines from the rendered parts (spec
1185
+ sections 4.3 and 4.4).
1186
+ """
788
1187
  if not isinstance(payload, dict) or payload.get("type") not in _RESPONSE_TOOL_OUTPUTS:
789
1188
  return None
790
1189
  value = payload["output"] if "output" in payload else payload.get("tools")
@@ -792,23 +1191,41 @@ def decode_tool_output_card(
792
1191
  budget = _TextBudget(text_cap)
793
1192
  parts: list[dict] = []
794
1193
  status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
795
- for index, part in enumerate(values[:_CARD_MAX_PARTS]):
796
- if isinstance(part, str):
797
- text = part
798
- match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
799
- if match is not None:
800
- status = "completed" if match.group(1) == "Script completed" else "failed"
801
- continue
802
- parts.append({"type": "text", "stream": "output", "text": budget.take(text)})
803
- continue
804
- if isinstance(part, dict) and part.get("type") == "input_text" \
805
- and isinstance(part.get("text"), str):
806
- text = part["text"]
807
- match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
1194
+ fields: dict = {}
1195
+ preamble = None
1196
+ drop_head = False
1197
+ head = _output_head_text(values[0]) if values else None
1198
+ if head is not None:
1199
+ if for_storage:
1200
+ match = _HARNESS_STATUS_RE.fullmatch(head)
808
1201
  if match is not None:
809
- status = "completed" if match.group(1) == "Script completed" else "failed"
810
- continue
811
- stream = part.get("stream") if part.get("stream") in {"stdout", "stderr"} else "output"
1202
+ status = ("completed" if match.group(1) == "Script completed"
1203
+ else "failed")
1204
+ drop_head = True
1205
+ else:
1206
+ preamble = parse_harness_preamble(head)
1207
+ if preamble is not None:
1208
+ fields = preamble[0]
1209
+ # A resolved state is evidence and wins. `unknown` adds nothing,
1210
+ # so it leaves `status` alone — which is what keeps the query
1211
+ # layer's unknown-gated backfill covering the remainder (4.6).
1212
+ if fields["status"] != "unknown":
1213
+ status = fields["status"]
1214
+ drop_head = not preamble[1]
1215
+
1216
+ for index, part in enumerate(values[:_CARD_MAX_PARTS]):
1217
+ if isinstance(part, str) or (
1218
+ isinstance(part, dict) and part.get("type") == "input_text"
1219
+ and isinstance(part.get("text"), str)):
1220
+ text = part if isinstance(part, str) else part["text"]
1221
+ if index == 0 and head is not None:
1222
+ if drop_head:
1223
+ continue
1224
+ if preamble is not None:
1225
+ text = preamble[1]
1226
+ stream = "output"
1227
+ if isinstance(part, dict) and part.get("stream") in {"stdout", "stderr"}:
1228
+ stream = part["stream"]
812
1229
  parts.append({"type": "text", "stream": stream, "text": budget.take(text)})
813
1230
  continue
814
1231
  raw = _canonical_json(part)
@@ -821,50 +1238,373 @@ def decode_tool_output_card(
821
1238
  "is_error": status in {"failed", "error"},
822
1239
  "parts": parts, "truncated": budget.truncated,
823
1240
  }
1241
+ if not for_storage:
1242
+ # Null when the grammar did not supply it. No `chunk_id` and no
1243
+ # `session_id`: a per-call hash has no reading value, and the session
1244
+ # identity a reader sees is the conversation-local ordinal from the
1245
+ # detail envelope's index, never the provider's own id (section 4.3).
1246
+ card["exit_code"] = fields.get("exit_code")
1247
+ card["wall_time_seconds"] = fields.get("wall_time_seconds")
824
1248
  return card, "".join(part["text"] for part in parts)
825
1249
 
826
1250
 
827
- def decode_patch_event_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
828
- """Bounded, provider-truthful ``patch_apply_end`` projection."""
1251
+ # The keys the dict-shaped `changes` entry is known to carry. Anything else goes
1252
+ # to `raw_extra`, so a provider addition is preserved rather than dropped.
1253
+ _PATCH_CHANGE_KEYS = frozenset({"type", "unified_diff", "content", "move_path"})
1254
+
1255
+
1256
+ def _synthesized_unified_diff(path: str, kind: str, content: str) -> str:
1257
+ """A unified diff for an ``add`` or ``delete`` entry, from its `content`.
1258
+
1259
+ The bytes are specified exactly because ``NativePatchCard`` does not render
1260
+ prefixed lines — it requires a parseable hunk. An empty file emits a
1261
+ zero-length hunk rather than a malformed one, and content that does not end
1262
+ in a newline gets the ``\`` marker so the stated
1263
+ line count and the rendered result agree.
1264
+
1265
+ This is a rendering of retained content, not something the provider sent,
1266
+ which is why every entry built from it is marked ``diff_source: derived``.
1267
+ """
1268
+ lines = content.split("\n")
1269
+ if lines and lines[-1] == "":
1270
+ lines.pop()
1271
+ complete = True
1272
+ else:
1273
+ complete = False
1274
+ count = len(lines)
1275
+ if kind == "add":
1276
+ header = f"--- /dev/null\n+++ {path}\n"
1277
+ hunk = "@@ -0,0 +0,0 @@\n" if count == 0 else f"@@ -0,0 +1,{count} @@\n"
1278
+ body = "".join(f"+{line}\n" for line in lines)
1279
+ else:
1280
+ header = f"--- {path}\n+++ /dev/null\n"
1281
+ hunk = "@@ -0,0 +0,0 @@\n" if count == 0 else f"@@ -1,{count} +0,0 @@\n"
1282
+ body = "".join(f"-{line}\n" for line in lines)
1283
+ marker = "" if complete or count == 0 else "\\n"
1284
+ return header + hunk + body + marker
1285
+
1286
+
1287
+ def _clip_to_line_boundary(text: str, limit: int) -> tuple[str, bool]:
1288
+ """``(text, was_cut)``, cut at a line boundary so the result still parses."""
1289
+ if len(text) <= limit:
1290
+ return text, False
1291
+ cut = text.rfind("\n", 0, limit)
1292
+ return (text[:cut + 1] if cut >= 0 else ""), True
1293
+
1294
+
1295
+ def _diff_body_survived(diff: str) -> bool:
1296
+ """True iff ``diff`` still carries a line a hunk can render.
1297
+
1298
+ `has_diff` means a diff is RENDERABLE (spec section 3.1), and a clip that
1299
+ keeps only the `---`/`+++`/`@@` headers renders nothing at all — the very
1300
+ shape a minified single-line file produces, because the whole file body is
1301
+ one physical line and the clip cuts at a line boundary. The same clip can
1302
+ also keep nothing at all, when the first line is already longer than the
1303
+ share.
1304
+
1305
+ A diff that was NOT cut is never tested against this, so the deliberate
1306
+ zero-length hunk an empty file emits (`@@ -0,0 +0,0 @@` with no body, spec
1307
+ section 3.1) keeps its diff.
1308
+ """
1309
+ seen_hunk = False
1310
+ for line in diff.split("\n"):
1311
+ if line.startswith("@@"):
1312
+ seen_hunk = True
1313
+ elif seen_hunk and line[:1] in ("+", "-", " ", "\\"):
1314
+ return True
1315
+ return False
1316
+
1317
+
1318
+ def _allocate_diff_budget(entries: list[dict], diffs: list[str | None],
1319
+ budget: _TextBudget) -> None:
1320
+ """Divide what is left of the shared budget equally among the files.
1321
+
1322
+ The single 16,000-character budget covers every file plus stdout and stderr,
1323
+ so a per-file cap is an ALLOCATION of that budget rather than an addition to
1324
+ it. A file needing less returns the remainder to the pool for later files; a
1325
+ file exceeding its share is cut at a line boundary and sets its own
1326
+ `truncated`. This is what stops the average 20,859-character deletion from
1327
+ consuming the budget belonging to the other files in the 487 events that
1328
+ touch two or more files.
1329
+
1330
+ A cut that leaves nothing renderable publishes NO ``unified_diff`` at all,
1331
+ only `truncated` and `diff_source`, so the client never receives a key it
1332
+ cannot render while `has_diff` claims it can.
1333
+ """
1334
+ pending = sum(1 for diff in diffs if diff is not None)
1335
+ for entry, diff in zip(entries, diffs):
1336
+ if diff is None:
1337
+ continue
1338
+ share = budget.remaining // pending if pending else 0
1339
+ kept, was_cut = _clip_to_line_boundary(diff, share)
1340
+ pending -= 1
1341
+ entry["truncated"] = was_cut
1342
+ if was_cut:
1343
+ budget.truncated = True
1344
+ if not _diff_body_survived(kept):
1345
+ continue
1346
+ entry["unified_diff"] = budget.take(kept)
1347
+
1348
+
1349
+ def _patch_files_from_changes_map(changes: dict, budget: _TextBudget) -> list[dict]:
1350
+ """The dict-shaped ``changes`` branch (spec section 3.1).
1351
+
1352
+ The path comes from the dict KEY and the status from the entry's ``type``,
1353
+ not from ``path``/``status`` — the two shapes disagree on both, which is why
1354
+ a direct port of the list loop would yield `(unknown file)` and a null status
1355
+ on every entry.
1356
+ """
1357
+ entries: list[dict] = []
1358
+ diffs: list[str | None] = []
1359
+ for path, change in list(changes.items())[:_CARD_MAX_FILES]:
1360
+ if not isinstance(path, str):
1361
+ continue
1362
+ # Per-file `truncated` is a wire field on EVERY entry (spec section 3.1),
1363
+ # so it is set here rather than only where a diff exists. Without it a
1364
+ # client would have to treat absent as false on the entries that carry no
1365
+ # diff, which is not what the spec describes.
1366
+ entry: dict[str, Any] = {"path": budget.take(path), "truncated": False}
1367
+ if not isinstance(change, dict):
1368
+ entry["raw"] = budget.take(_canonical_json(change))
1369
+ entries.append(entry)
1370
+ diffs.append(None)
1371
+ continue
1372
+ kind = change.get("type")
1373
+ if isinstance(kind, str):
1374
+ entry["status"] = budget.take(kind)
1375
+ if isinstance(change.get("move_path"), str):
1376
+ entry["move_path"] = budget.take(change["move_path"])
1377
+ diff: str | None = None
1378
+ if isinstance(change.get("unified_diff"), str):
1379
+ diff = change["unified_diff"]
1380
+ entry["diff_source"] = "retained"
1381
+ elif kind in {"add", "delete"} and isinstance(change.get("content"), str):
1382
+ # The PROVIDER's path, never `entry["path"]`, which the shared budget
1383
+ # may have clipped. A `---`/`+++` header naming a truncated path
1384
+ # would make the one card family whose purpose is provider-truth
1385
+ # assert a file that does not exist; the synthesized diff is clipped
1386
+ # by the shared allocation below, exactly as a retained one is.
1387
+ diff = _synthesized_unified_diff(path, kind, change["content"])
1388
+ entry["diff_source"] = "derived"
1389
+ unknown = {key: value for key, value in change.items()
1390
+ if key not in _PATCH_CHANGE_KEYS}
1391
+ if unknown:
1392
+ entry["raw_extra"] = budget.take(_canonical_json(unknown))
1393
+ entries.append(entry)
1394
+ diffs.append(diff)
1395
+ _allocate_diff_budget(entries, diffs, budget)
1396
+ return entries
1397
+
1398
+
1399
+ def decode_patch_event_card(
1400
+ payload: dict, *, text_cap: int = CODEX_TEXT_CAP, for_storage: bool = False,
1401
+ ) -> dict | None:
1402
+ """Bounded, provider-truthful ``patch_apply_end`` projection.
1403
+
1404
+ ``for_storage=True`` produces the FROZEN v1 projection ``_extract``
1405
+ persists (spec section 3.0): a dict-shaped ``changes`` object becomes one
1406
+ ``{"raw": …}`` entry, as it does today. Persisting the decoded per-file
1407
+ diffs would be the largest `detail_json` growth in S3 and would move
1408
+ `PAGE_SOURCE_BYTE_BUDGET` boundaries for newly ingested rows only.
1409
+ """
829
1410
  if not isinstance(payload, dict) or payload.get("type") != "patch_apply_end":
830
1411
  return None
831
1412
  budget = _TextBudget(text_cap)
832
1413
  files: list[dict] = []
833
1414
  changes = payload.get("changes")
834
- if isinstance(changes, list):
1415
+ stdout: str | None = None
1416
+ stderr: str | None = None
1417
+ streams_taken = False
1418
+ if not for_storage and isinstance(changes, dict):
1419
+ # stdout and stderr come off the shared budget FIRST here, because what
1420
+ # is left is what the per-file allocation divides.
1421
+ stdout = (budget.take(payload["stdout"])
1422
+ if isinstance(payload.get("stdout"), str) else None)
1423
+ stderr = (budget.take(payload["stderr"])
1424
+ if isinstance(payload.get("stderr"), str) else None)
1425
+ streams_taken = True
1426
+ files = _patch_files_from_changes_map(changes, budget)
1427
+ if len(changes) > _CARD_MAX_FILES:
1428
+ budget.truncated = True
1429
+ elif isinstance(changes, list):
835
1430
  for change in changes[:_CARD_MAX_FILES]:
836
1431
  if not isinstance(change, dict):
837
- files.append({"raw": budget.take(_canonical_json(change))})
1432
+ blob = _canonical_json(change)
1433
+ entry = {"raw": budget.take(blob)}
1434
+ if not for_storage:
1435
+ entry["truncated"] = len(entry["raw"]) != len(blob)
1436
+ files.append(entry)
838
1437
  continue
839
1438
  entry: dict[str, Any] = {}
1439
+ entry_cut = False
840
1440
  for key in ("path", "move_path", "status"):
841
1441
  if isinstance(change.get(key), str):
842
1442
  entry[key] = budget.take(change[key])
1443
+ entry_cut = entry_cut or len(entry[key]) != len(change[key])
843
1444
  if isinstance(change.get("unified_diff"), str):
844
1445
  entry["unified_diff"] = budget.take(change["unified_diff"])
1446
+ entry_cut = entry_cut or (
1447
+ len(entry["unified_diff"]) != len(change["unified_diff"]))
1448
+ # The provider transmitted this one; the card must not claim a
1449
+ # synthesized diff and a retained diff are the same thing.
1450
+ # Read time ONLY: this is an added field, and adding it to the
1451
+ # stored card would grow `detail_json` for newly ingested rows
1452
+ # while historical rows kept their old estimates, which is
1453
+ # precisely the page-boundary drift spec section 3.0 forbids.
1454
+ if not for_storage:
1455
+ entry["diff_source"] = "retained"
845
1456
  unknown = {key: value for key, value in change.items()
846
1457
  if key not in {"path", "move_path", "status", "unified_diff"}}
847
1458
  if unknown:
848
- entry["raw_extra"] = budget.take(_canonical_json(unknown))
1459
+ blob = _canonical_json(unknown)
1460
+ entry["raw_extra"] = budget.take(blob)
1461
+ entry_cut = entry_cut or len(entry["raw_extra"]) != len(blob)
1462
+ # Per-file `truncated` on EVERY entry (spec section 3.1), read time
1463
+ # only for the same reason `diff_source` is.
1464
+ if not for_storage:
1465
+ entry["truncated"] = entry_cut
849
1466
  files.append(entry)
850
1467
  if len(changes) > _CARD_MAX_FILES:
851
1468
  budget.truncated = True
852
1469
  elif "changes" in payload:
853
- files.append({"raw": budget.take(_canonical_json(changes))})
854
- stdout = budget.take(payload["stdout"]) if isinstance(payload.get("stdout"), str) else None
855
- stderr = budget.take(payload["stderr"]) if isinstance(payload.get("stderr"), str) else None
1470
+ blob = _canonical_json(changes)
1471
+ entry = {"raw": budget.take(blob)}
1472
+ if not for_storage:
1473
+ entry["truncated"] = len(entry["raw"]) != len(blob)
1474
+ files.append(entry)
1475
+ if not streams_taken:
1476
+ stdout = (budget.take(payload["stdout"])
1477
+ if isinstance(payload.get("stdout"), str) else None)
1478
+ stderr = (budget.take(payload["stderr"])
1479
+ if isinstance(payload.get("stderr"), str) else None)
856
1480
  status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
857
1481
  success = payload.get("success") if isinstance(payload.get("success"), bool) else None
858
1482
  return {
859
1483
  "schema_version": CODEX_CARD_SCHEMA_VERSION,
860
1484
  "type": "patch", "source": "patch_apply_end",
861
1485
  "files": files,
862
- "has_diff": any(isinstance(entry.get("unified_diff"), str) for entry in files),
1486
+ # RENDERABLE, not merely present (spec section 3.1). An entry whose clip
1487
+ # kept nothing a hunk can render publishes no `unified_diff` at all, and
1488
+ # a provider that transmits an empty one must not flip this to true.
1489
+ "has_diff": any(entry.get("unified_diff") for entry in files),
863
1490
  "status": status, "success": success, "stdout": stdout, "stderr": stderr,
864
1491
  "truncated": budget.truncated,
865
1492
  }
866
1493
 
867
1494
 
1495
+ # ── the external-agent marker (F9, spec section 5.5) ─────────────────────────
1496
+ #
1497
+ # Anchored at the start of a line. The name is 1-128 characters excluding `]` and
1498
+ # newline; the next line must open with `input: ` and carry a JSON value.
1499
+ # Validation is all-or-nothing: a name that does not match, an absent `input:`
1500
+ # line, or JSON that does not parse leaves the block as ordinary prose.
1501
+ _EXTERNAL_CALL_RE = re.compile(
1502
+ r"^\[external_agent_tool_call: ([^\]\n]{1,128})\]\n", re.MULTILINE)
1503
+ _EXTERNAL_CALL_INPUT = "input: "
1504
+
1505
+
1506
+ def _inside_fenced_block(text: str, position: int) -> bool:
1507
+ """True when ``position`` falls inside an open Markdown fence.
1508
+
1509
+ A marker inside a fenced code block is authored content — someone writing
1510
+ ABOUT the grammar — rather than a serialized call, so it must stay prose.
1511
+ Same toggle rule `_segment_harness_markers` already uses.
1512
+ """
1513
+ fence: str | None = None
1514
+ for line in text[:position].splitlines():
1515
+ opened = re.match(r"\s*(`{3,}|~{3,})", line)
1516
+ if opened is None:
1517
+ continue
1518
+ char = opened.group(1)[0]
1519
+ if fence is None:
1520
+ fence = char
1521
+ elif fence == char:
1522
+ fence = None
1523
+ return fence is not None
1524
+
1525
+
1526
+ def _external_call_from_text(
1527
+ text: str, *, text_cap: int = CODEX_TEXT_CAP,
1528
+ ) -> dict | None:
1529
+ """The `external_agent_tool_call` marker on an assistant row, or ``None``.
1530
+
1531
+ This runs at READ time over the row's stored text and needs no payload load,
1532
+ which is the only way it can reach the data it targets: all 3,789 markers are
1533
+ historical, dated 2026-07-11 across 46 conversations, and the grammar has not
1534
+ recurred — so an ingest-time implementation would detect nothing at all and
1535
+ would ship as a no-op against the only rows it exists for.
1536
+
1537
+ The caller writes this to ``detail.external_call`` and must NEVER write it to
1538
+ ``detail.markers``, because that key selects which rows the export path
1539
+ hydrates and 729 assistant rows already carry it.
1540
+
1541
+ ``span`` is the half-open ``[start, end)`` character range of the marker run
1542
+ the card consumed, measured in the SAME string this was handed — which is the
1543
+ string the caller serves as ``block["text"]``. The export keeps that prose
1544
+ verbatim and its bytes are frozen, so the client has no other way to locate
1545
+ what the card already renders, and without the span the viewer would show the
1546
+ marker twice. The range covers the ``[external_agent_tool_call: …]`` line,
1547
+ the ``input:`` line and the JSON value, plus one immediately following
1548
+ newline when there is one, so removing it leaves no orphaned blank line.
1549
+ """
1550
+ if not isinstance(text, str) or not text:
1551
+ return None
1552
+ if len(text) > _CARD_HARNESS_PARSE_CAP:
1553
+ return None
1554
+ match = _EXTERNAL_CALL_RE.search(text)
1555
+ if match is None:
1556
+ return None
1557
+ if _inside_fenced_block(text, match.start()):
1558
+ return None
1559
+ rest = text[match.end():]
1560
+ if not rest.startswith(_EXTERNAL_CALL_INPUT):
1561
+ return None
1562
+ try:
1563
+ value, end = json.JSONDecoder().raw_decode(rest, len(_EXTERNAL_CALL_INPUT))
1564
+ except (json.JSONDecodeError, TypeError, ValueError):
1565
+ return None
1566
+ start = match.start()
1567
+ end += match.end()
1568
+ if end < len(text) and text[end] == "\n":
1569
+ end += 1
1570
+ if not 0 <= start < end <= len(text):
1571
+ # Fail closed. A span that does not resolve is worse than no card: the
1572
+ # client would hide the wrong run of text, or none at all.
1573
+ return None
1574
+ budget = _TextBudget(text_cap)
1575
+ return {
1576
+ "schema_version": CODEX_CARD_SCHEMA_VERSION,
1577
+ "name": budget.take(match.group(1)),
1578
+ "input": _bounded_json(value, budget),
1579
+ "truncated": budget.truncated,
1580
+ "span": [start, end],
1581
+ }
1582
+
1583
+
1584
+ def external_call_span_resolves(text: Any, external: Any) -> bool:
1585
+ """True iff ``external``'s span addresses its own marker inside ``text``.
1586
+
1587
+ The assembler calls this against the exact string it is about to serve as
1588
+ ``block["text"]``, and withholds the card when it returns False. That is the
1589
+ fail-closed half of the span contract: a marker the served text does not
1590
+ wholly contain — because the row's text was capped at ingest, or because a
1591
+ later stage replaced it — yields no ``external_call`` at all rather than a
1592
+ span pointing at the wrong characters.
1593
+ """
1594
+ if not isinstance(text, str) or not isinstance(external, dict):
1595
+ return False
1596
+ span = external.get("span")
1597
+ if not isinstance(span, list) or len(span) != 2:
1598
+ return False
1599
+ start, end = span
1600
+ for bound in (start, end):
1601
+ if not isinstance(bound, int) or isinstance(bound, bool):
1602
+ return False
1603
+ if not 0 <= start < end <= len(text):
1604
+ return False
1605
+ return text[start:end].startswith("[external_agent_tool_call: ")
1606
+
1607
+
868
1608
  @dataclasses.dataclass
869
1609
  class _Extracted:
870
1610
  kind: str
@@ -1242,15 +1982,22 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
1242
1982
  args = _stringify(payload.get("input") or payload.get("arguments"))
1243
1983
  text = f"{name}\n{args}" if args else str(name)
1244
1984
  detail = {"name": name, "args": args[:CODEX_TEXT_CAP]}
1245
- card = decode_tool_call_card(payload)
1985
+ # Frozen v1 projection at ingest (spec section 3.0) — see the
1986
+ # `for_storage` note on `decode_tool_output_card` above.
1987
+ card = decode_tool_call_card(payload, for_storage=True)
1246
1988
  if card is None:
1247
- card = decode_secondary_tool_call_card(payload)
1989
+ card = decode_secondary_tool_call_card(payload, for_storage=True)
1248
1990
  if card is not None:
1249
1991
  detail["card"] = card
1250
1992
  return _Extracted("tool_call", text, "search_tool", detail, [])
1251
1993
  if ptype in _RESPONSE_TOOL_OUTPUTS:
1252
1994
  value = payload["output"] if "output" in payload else payload.get("tools")
1253
- shaped = decode_tool_output_card(payload)
1995
+ # `for_storage=True`: the stored card is the FROZEN v1 projection
1996
+ # (spec section 3.0). Every S3 enrichment is computed at read time,
1997
+ # because `detail_json` bytes feed `_row_source_bytes` and therefore
1998
+ # page boundaries, and a richer stored card would paginate a newly
1999
+ # ingested conversation differently from an identical historical one.
2000
+ shaped = decode_tool_output_card(payload, for_storage=True)
1254
2001
  if shaped is not None:
1255
2002
  card, _display_body = shaped
1256
2003
  return _Extracted(
@@ -1287,7 +2034,9 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
1287
2034
  detail = {"event": ptype}
1288
2035
  if ptype in {"task_started", "task_complete"}:
1289
2036
  detail.update(_lifecycle_projection(payload))
1290
- patch_card = decode_patch_event_card(payload)
2037
+ # Frozen v1 projection at ingest (spec section 3.0) — see the
2038
+ # `for_storage` note on `decode_tool_output_card` above.
2039
+ patch_card = decode_patch_event_card(payload, for_storage=True)
1291
2040
  card = patch_card or decode_secondary_event_card(payload)
1292
2041
  if card is not None:
1293
2042
  detail["card"] = card
@@ -1304,13 +2053,8 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
1304
2053
  text = " ".join(p for p in ("task_complete", _stringify(
1305
2054
  payload.get("last_agent_message"))) if p)
1306
2055
  elif ptype == "patch_apply_end":
1307
- paths = []
1308
- changes = payload.get("changes")
1309
- if isinstance(changes, list):
1310
- for change in changes:
1311
- if isinstance(change, dict) and isinstance(change.get("path"), str):
1312
- paths.append(change["path"])
1313
- touches.append((change["path"], "apply_patch"))
2056
+ paths = codex_patch_file_paths(payload)
2057
+ touches.extend((path, "apply_patch") for path in paths)
1314
2058
  text = " ".join(["patch_apply", *paths]) if paths else "patch_apply"
1315
2059
  elif ptype == "mcp_tool_call_end":
1316
2060
  invocation = payload.get("invocation")
@@ -1328,6 +2072,32 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
1328
2072
  return text, touches
1329
2073
 
1330
2074
 
2075
+ def codex_patch_file_paths(payload: Any) -> list[str]:
2076
+ """Return provider-ordered file paths from one patch completion.
2077
+
2078
+ Current Codex emits ``changes`` as an object keyed by path; the legacy list
2079
+ shape carries the path on each entry. Only object-valued change entries are
2080
+ touches in either shape, matching the read-time patch derivation contract.
2081
+ """
2082
+ if not isinstance(payload, dict) or payload.get("type") != "patch_apply_end":
2083
+ return []
2084
+ changes = payload.get("changes")
2085
+ if isinstance(changes, dict):
2086
+ items = changes.items()
2087
+ elif isinstance(changes, list):
2088
+ items = (
2089
+ (change.get("path"), change)
2090
+ for change in changes
2091
+ if isinstance(change, dict)
2092
+ )
2093
+ else:
2094
+ return []
2095
+ return [
2096
+ path for path, change in items
2097
+ if isinstance(path, str) and isinstance(change, dict)
2098
+ ]
2099
+
2100
+
1331
2101
  def infer_codex_event_turns(
1332
2102
  events: Iterable[Any], *, initial_turn: str | None = None
1333
2103
  ) -> tuple[list[str | None], str | None]: