Skip to content

Commit 77284db

Browse files
jackaldenryancursoragent
andcommitted
fix(ingestion): compare document tails as instants, not RFC3339 strings
Email Date headers mix timezone offsets; lexicographic created_at order could poll the wrong saga. Post-release smoke now checks out the release SHA. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8c0975f commit 77284db

3 files changed

Lines changed: 81 additions & 9 deletions

File tree

.github/workflows/smoke-ingestion.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ jobs:
2626
working-directory: ingestion
2727
steps:
2828
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
# workflow_run otherwise checks out current default-branch HEAD, not
31+
# the commit the Release Ingestion Package workflow ran on.
32+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
2933

3034
- name: Install uv
3135
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3

ingestion/src/zep_ingest/result.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
from collections.abc import Sequence
55
from dataclasses import dataclass, field
6+
from datetime import datetime, timezone
67
from typing import TYPE_CHECKING, Any, Literal
78

89
from zep_ingest._validation import require_int_range, require_nonnegative_number
@@ -41,6 +42,32 @@
4142
]
4243

4344

45+
def _parse_created_at(value: str | None) -> datetime | None:
46+
"""Parse an episode ``created_at`` into a comparable UTC instant."""
47+
if not value:
48+
return None
49+
try:
50+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
51+
except ValueError:
52+
return None
53+
if parsed.tzinfo is None:
54+
return parsed.replace(tzinfo=timezone.utc)
55+
return parsed
56+
57+
58+
def _is_later_or_equal_created_at(new_at: str | None, prior_at: str | None) -> bool:
59+
"""Whether ``new_at`` is the later document tail.
60+
61+
Missing or unparseable timestamps fall back to last-submitted-wins. Offsets
62+
are compared as instants, not lexicographic RFC3339 strings.
63+
"""
64+
new_dt = _parse_created_at(new_at)
65+
prior_dt = _parse_created_at(prior_at)
66+
if new_dt is None or prior_dt is None:
67+
return True
68+
return new_dt >= prior_dt
69+
70+
4471
def _normalize_task_status(status: str | None) -> str:
4572
status = status.lower() if status is not None else None
4673
if status is None or status in {"created", "draft", "pending", "queued"}:
@@ -178,11 +205,7 @@ def record_sequential_episode(self, episode: Episode, uuid: str) -> None:
178205
self._uses_document_grouping = True
179206
prior_at = self._document_poll_created_at.get(episode.document_id)
180207
new_at = episode.created_at
181-
if prior_at is None or new_at is None:
182-
# Missing timestamps: fall back to submission order (last wins).
183-
self._document_poll_uuids[episode.document_id] = uuid
184-
self._document_poll_created_at[episode.document_id] = new_at
185-
elif new_at >= prior_at:
208+
if _is_later_or_equal_created_at(new_at, prior_at):
186209
self._document_poll_uuids[episode.document_id] = uuid
187210
self._document_poll_created_at[episode.document_id] = new_at
188211
else:
@@ -362,10 +385,7 @@ def combine(self, *others: "IngestResult") -> "IngestResult":
362385
for document_id, uuid in part._document_poll_uuids.items():
363386
created_at = part._document_poll_created_at.get(document_id)
364387
prior_at = combined._document_poll_created_at.get(document_id)
365-
if prior_at is None or created_at is None:
366-
combined._document_poll_uuids[document_id] = uuid
367-
combined._document_poll_created_at[document_id] = created_at
368-
elif (created_at or "") >= (prior_at or ""):
388+
if _is_later_or_equal_created_at(created_at, prior_at):
369389
combined._document_poll_uuids[document_id] = uuid
370390
combined._document_poll_created_at[document_id] = created_at
371391
if part._plain_episode_tail is not None:

ingestion/tests/test_document_poll.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,51 @@ def test_document_poll_tail_submission_order_when_created_at_missing():
3030
)
3131
result.finalize_sequential_episode_poll()
3232
assert result.episode_uuids == ["uuid-second"]
33+
34+
35+
def test_document_poll_tail_compares_instants_not_rfc3339_strings():
36+
"""Offsets that sort the wrong way lexicographically still pick the later instant."""
37+
result = IngestResult(method="sequential")
38+
result.record_sequential_episode(
39+
Episode(
40+
data="later Pacific",
41+
document_id="doc-1",
42+
created_at="2024-06-15T18:00:00-07:00",
43+
),
44+
"uuid-later",
45+
)
46+
result.record_sequential_episode(
47+
Episode(
48+
data="earlier UTC",
49+
document_id="doc-1",
50+
created_at="2024-06-15T20:00:00+00:00",
51+
),
52+
"uuid-earlier",
53+
)
54+
result.finalize_sequential_episode_poll()
55+
assert result.episode_uuids == ["uuid-later"]
56+
57+
58+
def test_combine_document_poll_tail_uses_later_instant():
59+
first = IngestResult(method="sequential")
60+
first.record_sequential_episode(
61+
Episode(
62+
data="later Pacific",
63+
document_id="doc-1",
64+
created_at="2024-06-15T18:00:00-07:00",
65+
),
66+
"uuid-later",
67+
)
68+
first.finalize_sequential_episode_poll()
69+
second = IngestResult(method="sequential")
70+
second.record_sequential_episode(
71+
Episode(
72+
data="earlier UTC",
73+
document_id="doc-1",
74+
created_at="2024-06-15T20:00:00+00:00",
75+
),
76+
"uuid-earlier",
77+
)
78+
second.finalize_sequential_episode_poll()
79+
combined = first.combine(second)
80+
assert combined.episode_uuids == ["uuid-later"]

0 commit comments

Comments
 (0)