Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions loopx/control_plane/runtime/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ def parse_timestamp(value: Any) -> datetime | None:
parsed = datetime.fromisoformat(
stable_text.replace("Z", "+00:00").replace("z", "+00:00")
)
except ValueError:
if parsed.tzinfo is None:
return parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Align the typed expiry codec before treating UTC overflow as missing time

A non-watch-only Monitor is configured with expires_at="0001-01-01T00:00:00+14:00" (also reproduced at the upper boundary), no resume condition, and an ordinary due time. The typed plan and public update commit the out-of-range expiry. The new Python None result is interpreted as absent expiry: readback succeeds, monitor_todo_is_expired=false and monitor_todo_is_due=true despite watch_only=false. Typed validation and retained-reader time meaning diverge, so the required expiry does not bound that consumer.

Extend the existing TS parseTodoTimestampMicros owner to reject UTC results outside the retained 0001..9999 range before the monitor update commits. Keep the Python exception normalization, and cover typed planning plus public update/no-write rejection and retained-reader expiry/due parity; do not introduce a second expiry owner.

Regression: Run todo.monitor_metadata.plan and public update_goal_todo with both extreme offset expiry values and watch_only=false/no resume: expect normal timestamp rejection without state changes. Verify legal boundaries, corrected expiry readback, and identical Python/TS expiry/due meaning.

except (ValueError, OverflowError):
return None
if parsed.tzinfo is None:
return parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)


def chronology_key(value: Any) -> tuple[int, datetime, str]:
Expand Down
12 changes: 10 additions & 2 deletions loopx/control_plane/runtime_timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ export function parseTodoTimestampMicros(value: string): bigint | null {
calendar = parseIsoTimestamp(expanded);
if (!calendar || calendar.getUTCFullYear() < 1) return null;
}
if (time === undefined) return BigInt(calendar.valueOf()) * 1000n;
if (time === undefined) {
const result = BigInt(calendar.valueOf()) * 1000n;
if (result < -62135596800000000n || result >= 253402300800000000n) return null;
return result;
}
const parts = /^(.*?)(Z|z|[+-].*)?$/.exec(time)!;
function clock(raw: string, offset: boolean): bigint | null {
const parsed = /^(\d{2})(?:(:?)(\d{2})(?:\2(\d{2}))?)?(?:[.,](\d+))?$/.exec(raw);
Expand All @@ -87,5 +91,9 @@ export function parseTodoTimestampMicros(value: string): bigint | null {
if (parsed === null) return null;
offset = parts[2][0] === "-" ? -parsed : parsed;
}
return BigInt(calendar.valueOf()) * 1000n + local - offset;
const result = BigInt(calendar.valueOf()) * 1000n + local - offset;
// Python datetime requires UTC results within 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999Z.
// -62135596800000000n is 0001-01-01T00:00:00Z, 253402300800000000n is 10000-01-01T00:00:00Z.
if (result < -62135596800000000n || result >= 253402300800000000n) return null;
return result;
}
8 changes: 8 additions & 0 deletions tests/control_plane/test_monitor_state_owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ def test_monitor_timestamp_codec_is_stable_across_supported_python_versions(valu
assert result["metadata"]["expires_at"] == value


@pytest.mark.parametrize("value", [
"9999-12-31T23:59:59-14:00",
"0001-01-01T00:00:00+14:00",
])
def test_timestamp_outside_utc_datetime_range_returns_none(value):
assert parse_timestamp(value) is None


def test_monitor_timestamp_codec_rejects_version_specific_end_of_day():
value = "2030-01-01T24:00:00"
assert parse_timestamp(value) is None
Expand Down
3 changes: 3 additions & 0 deletions tests/control_plane_ts/monitor_metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ test("Todo timestamp codec retains Python ISO compatibility, timezone seconds an
for (const value of ["1970-02-30", "2021-W53", "1970-01-01T01:00+24:00",
"1970-01-01T24:00:00", "1970-01-01T24:00:01",
"9999-12-31T24:00:00", "9999-12-31T24:00:00Z", "9999-12-31T24:00:00.000000",
"0001-01-01T00:00:00+14:00", "9999-12-31T23:59:59-14:00",
"1970-01-01T00:0000", "1970-01-01T0000:00", "tomorrow", "2030"]) {
assert.equal(parseTodoTimestampMicros(value), null, value);
}
assert.equal(parseTodoTimestampMicros("0001-01-01T00:00:00Z"), -62135596800000000n);
assert.equal(parseTodoTimestampMicros("9999-12-31T23:59:59.999999Z"), 253402300799999999n);
});
Loading