Skip to content

Commit 82b8055

Browse files
committed
Merge remote-tracking branch 'origin/feature/ck08r3a-lifecycle-final-acceptance-a152c75' into integration/ck08r3a-pr417-b00232ac
2 parents b00232a + 71e3460 commit 82b8055

16 files changed

Lines changed: 1778 additions & 144 deletions

File tree

src/codex_usage_tracker/agent_kernel/domain/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class LifecycleTransition:
125125
terminal_error_category: str | None
126126
measurement_mask: int
127127
first_seen_publication_id: str
128+
session_id: str
128129

129130
def __post_init__(self) -> None:
130131
validate_nonnegative_int64(self.transition_version, allow_none=False)
@@ -136,6 +137,8 @@ def __post_init__(self) -> None:
136137
validate_nonnegative_int64(self.event_kind_order, allow_none=False)
137138
validate_nonnegative_int64(self.transition_rank, allow_none=False)
138139
validate_nonnegative_int64(self.measurement_mask, allow_none=False)
140+
if not isinstance(self.session_id, str) or not self.session_id.strip():
141+
raise ValueError("session_id must be a non-empty canonical identifier")
139142

140143

141144
@dataclass(frozen=True, slots=True)

src/codex_usage_tracker/agent_kernel/evidence/service.py

Lines changed: 279 additions & 116 deletions
Large diffs are not rendered by default.

src/codex_usage_tracker/agent_kernel/publication/preparation.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from collections import Counter, defaultdict
66
from collections.abc import Callable, Mapping
7+
from dataclasses import replace
78
from decimal import Decimal
89

910
from ..adapters.codex_jsonl.canonicalize import ProposedChangeSet
@@ -56,7 +57,10 @@ def __init__(
5657
inventory_started_at_us: int | None,
5758
inventory_completed_at_us: int | None,
5859
) -> None:
59-
self.changes = changes
60+
normalized_observations = tuple(
61+
self._normalize_source_order(observation) for observation in changes.observations
62+
)
63+
self.changes = replace(changes, observations=normalized_observations)
6064
self.request = request
6165
self.configured_producer_key = configured_producer_key
6266
self.prior = prior
@@ -77,6 +81,15 @@ def __init__(
7781
self.folds: dict[str, LifecycleFold] = {}
7882
self.tail_ordinal = 0 if prior.tail_state is None else prior.tail_state.row_count
7983

84+
@staticmethod
85+
def _normalize_source_order(observation: AdapterObservation) -> AdapterObservation:
86+
if observation.source_order is not None:
87+
return observation
88+
fallback = observation.source_range.record_ordinal
89+
if fallback is None:
90+
raise PublicationWriteError("source order provenance is missing")
91+
return replace(observation, source_order=fallback)
92+
8093
def prepare(self) -> PublicationWriteSet:
8194
self._add_adapter()
8295
coverage_items = self._add_inventories()
@@ -477,6 +490,15 @@ def _build_lifecycle(self) -> None:
477490
continue
478491
sequence[observation.logical_id] += 1
479492
version = sequence[observation.logical_id]
493+
session_id = (
494+
observation.logical_id
495+
if lifecycle_kind == "session"
496+
else observation.payload.get("session_id")
497+
)
498+
if not isinstance(session_id, str) or not session_id:
499+
raise PublicationWriteError(
500+
"lifecycle session provenance is missing"
501+
)
480502
transition_identity = [
481503
observation.logical_id,
482504
version,
@@ -506,6 +528,7 @@ def _build_lifecycle(self) -> None:
506528
),
507529
measurement_mask=observation.measurement_mask,
508530
first_seen_publication_id=self.publication_id,
531+
session_id=session_id,
509532
)
510533
)
511534
self.folds = {
@@ -639,6 +662,11 @@ def _add_turn(
639662
) -> None:
640663
payload = observation.payload
641664
fold = self._fold(logical_id)
665+
start_source_order = observation.source_order
666+
if start_source_order is None:
667+
start_source_order = observation.source_range.record_ordinal
668+
if start_source_order is None:
669+
raise PublicationWriteError("turn source order provenance is missing")
642670
self.rows.append(
643671
PreparedRow(
644672
"turns",
@@ -651,9 +679,10 @@ def _add_turn(
651679
"transition_version": fold.transition_version,
652680
"start_at_us": fold.start_at_us,
653681
"end_at_us": fold.terminal_at_us,
654-
"start_source_order": observation.source_order,
682+
"start_source_rank": observation.source_rank,
683+
"start_source_order": start_source_order,
655684
"end_source_order": (
656-
observation.source_order if fold.terminal_at_us is not None else None
685+
start_source_order if fold.terminal_at_us is not None else None
657686
),
658687
"completion_basis": None,
659688
"membership_json": "{}",

src/codex_usage_tracker/agent_kernel/publication/writer.py

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from types import MappingProxyType
1919
from typing import TYPE_CHECKING, Any, cast
2020

21-
from ..adapters.codex_jsonl.canonicalize import ProposedChangeSet
21+
from ..adapters.codex_jsonl.canonicalize import ProposedChangeSet, ProposedOccurrence
2222
from ..adapters.contracts import (
2323
ADAPTER_ID,
2424
ADAPTER_VERSION,
@@ -365,6 +365,7 @@ class _TableSpec:
365365
"transition_version",
366366
"start_at_us",
367367
"end_at_us",
368+
"start_source_rank",
368369
"start_source_order",
369370
"end_source_order",
370371
"completion_basis",
@@ -1391,8 +1392,8 @@ def _write_facts(
13911392
lifecycle: tuple[tuple[object, ...], ...],
13921393
rows: tuple[PreparedRow, ...],
13931394
) -> None:
1394-
self._insert_lifecycle_many(lifecycle)
13951395
self._apply_rows(rows)
1396+
self._insert_lifecycle_many(lifecycle)
13961397

13971398
def _write_metadata(
13981399
self,
@@ -1519,6 +1520,128 @@ def _validate_write_set(
15191520
)
15201521
if request.publication_id == plan.parent_publication_id:
15211522
raise PublicationWriteError("publication cannot parent itself")
1523+
self._validate_turn_provenance(write_set)
1524+
1525+
def _validate_turn_provenance(self, write_set: PublicationWriteSet) -> None:
1526+
"""Validate the persisted turn coordinate before any writer mutation.
1527+
1528+
A turn's primary occurrence is the only admissible bridge to its source
1529+
manifestation. The check stays on the prepared write set so an
1530+
invalid or mixed cohort cannot reach the transaction and rely on a
1531+
deferred foreign-key error after partial work.
1532+
"""
1533+
1534+
turn_rows = [row for row in write_set.rows if row.table == "turns"]
1535+
if not turn_rows:
1536+
return
1537+
1538+
observations: dict[str, list[AdapterObservation]] = {}
1539+
for observation in write_set.changes.observations:
1540+
if observation.observation_type == "TurnBoundaryObserved":
1541+
observations.setdefault(observation.logical_id, []).append(observation)
1542+
1543+
occurrences: dict[str, ProposedOccurrence] = {}
1544+
for occurrence in write_set.changes.occurrences:
1545+
occurrence_id = occurrence.occurrence_id
1546+
previous = occurrences.get(occurrence_id)
1547+
if previous is not None and previous != occurrence:
1548+
raise PublicationWriteError(
1549+
f"primary occurrence is ambiguous: {occurrence_id}"
1550+
)
1551+
occurrences[occurrence_id] = occurrence
1552+
1553+
inventories: dict[int, SourceInventory] = {}
1554+
for inventory in (
1555+
*write_set.changes.selected_sources,
1556+
*write_set.changes.deferred_sources,
1557+
):
1558+
previous_inventory = inventories.get(inventory.manifestation_key)
1559+
if previous_inventory is not None and previous_inventory != inventory:
1560+
raise PublicationWriteError(
1561+
"source manifestation is ambiguous: "
1562+
f"{inventory.manifestation_key}"
1563+
)
1564+
inventories[inventory.manifestation_key] = inventory
1565+
1566+
for row in turn_rows:
1567+
turn_id = str(row.values["turn_id"])
1568+
candidates = observations.get(turn_id, [])
1569+
if not candidates:
1570+
raise PublicationWriteError(
1571+
f"turn primary occurrence has no source observation: {turn_id}"
1572+
)
1573+
selected = max(candidates, key=lambda item: item.sort_key)
1574+
tied = [item for item in candidates if item.sort_key == selected.sort_key]
1575+
if len(tied) != 1:
1576+
raise PublicationWriteError(
1577+
f"turn primary occurrence is ambiguous: {turn_id}"
1578+
)
1579+
1580+
occurrence_id = str(row.values["primary_occurrence_id"])
1581+
if occurrence_id != selected.occurrence_id:
1582+
raise PublicationWriteError(
1583+
f"turn primary occurrence does not match observation: {turn_id}"
1584+
)
1585+
resolved_occurrence = occurrences.get(occurrence_id)
1586+
if resolved_occurrence is None:
1587+
raise PublicationWriteError(
1588+
f"turn primary occurrence is unresolved: {occurrence_id}"
1589+
)
1590+
if resolved_occurrence.semantic_logical_id != turn_id:
1591+
raise PublicationWriteError(
1592+
f"turn primary occurrence belongs to another entity: {turn_id}"
1593+
)
1594+
1595+
source = resolved_occurrence.source_range
1596+
resolved_inventory = inventories.get(source.manifestation_key)
1597+
if resolved_inventory is None:
1598+
raise PublicationWriteError(
1599+
"turn primary occurrence has no source manifestation: "
1600+
f"{occurrence_id}"
1601+
)
1602+
if (
1603+
resolved_inventory.manifestation_id != source.manifestation_id
1604+
or resolved_inventory.content_revision != source.source_revision
1605+
):
1606+
raise PublicationWriteError(
1607+
f"turn occurrence/manifestation provenance mismatches: {turn_id}"
1608+
)
1609+
1610+
expected_order = selected.source_order
1611+
if expected_order is None:
1612+
expected_order = source.record_ordinal
1613+
if expected_order is None:
1614+
raise PublicationWriteError(
1615+
f"turn source order provenance is missing: {turn_id}"
1616+
)
1617+
if selected.source_rank != resolved_inventory.source_rank:
1618+
raise PublicationWriteError(
1619+
f"turn source rank mismatches its manifestation: {turn_id}"
1620+
)
1621+
if row.values["start_source_rank"] != resolved_inventory.source_rank:
1622+
raise PublicationWriteError(
1623+
f"turn persisted source rank mismatches provenance: {turn_id}"
1624+
)
1625+
if row.values["start_source_order"] != expected_order:
1626+
raise PublicationWriteError(
1627+
f"turn persisted source order mismatches provenance: {turn_id}"
1628+
)
1629+
1630+
start_at_us = row.values["start_at_us"]
1631+
end_at_us = row.values["end_at_us"]
1632+
end_source_order = row.values["end_source_order"]
1633+
if end_at_us is not None and start_at_us is not None and start_at_us > end_at_us:
1634+
raise PublicationWriteError(
1635+
f"turn lifecycle times are reversed: {turn_id}"
1636+
)
1637+
if end_source_order is not None and end_source_order < expected_order:
1638+
raise PublicationWriteError(
1639+
f"turn lifecycle source order is reversed: {turn_id}"
1640+
)
1641+
if end_at_us is not None and end_source_order is None:
1642+
raise PublicationWriteError(
1643+
f"terminal turn is missing end source order: {turn_id}"
1644+
)
15221645

15231646
def _recheck_parent(self, expected: str | None) -> None:
15241647
row = self._connection.execute(
@@ -1696,6 +1819,7 @@ def _lifecycle_values(
16961819
transition.terminal_error_category,
16971820
transition.measurement_mask,
16981821
transition.first_seen_publication_id,
1822+
transition.session_id,
16991823
)
17001824

17011825
def _validate_existing_identities(

0 commit comments

Comments
 (0)