ADR-002: Explicit millisecond simulation time

Date: 2026-07-19

Status: Accepted

Context

The architecture for v0.1.0 left the exact time representation open in section 9.2. Section 8.1 required a representation with documented range and overflow behaviour, safe handling of invalid input, deterministic transitions, independence from frame rate, and practical use on embedded targets.

PBI-017 requires that Pet Core advances only from caller-provided time values, that equal inputs produce equal results, that invalid time cannot partially mutate a world, and that time processing performs bounded work with no allocation.

A host measures time and passes it in. The question was what unit and width to accept, what to do with an implausibly large delta, and what time state a world must keep.

Decision

PetTimeMs is uint64_t and carries a duration in milliseconds. The host passes an elapsed delta rather than an absolute clock value, so Pet Core never has to interpret a calendar, a time zone, or an epoch.

pet_world_update applies one delta to one world. A world keeps elapsed_time_ms and update_count as the observable time state, matching the domain model in architecture section 5.5.

Accepted behaviour:

  • a zero delta succeeds and changes nothing, because time did not move
  • PET_TIME_DELTA_MAX_MS is one day, and a larger delta is rejected with PET_ERR_INVALID_ARGUMENT
  • a delta that would overflow elapsed_time_ms is rejected with PET_ERR_INVALID_ARGUMENT
  • an accepted delta is applied in a single step and no missed time is replayed
  • the world is validated before the delta, so a corrupt world reports PET_ERR_INVALID_STATE even when the delta is also out of range
  • everything is validated before anything is written, so a rejected update leaves the world byte for byte untouched

No new status value was introduced. PET_ERR_INVALID_ARGUMENT already describes a value outside its accepted range.

Consequences

Milliseconds are fine enough for interactive behaviour and coarse enough that a 64-bit counter cannot realistically overflow. At the maximum accepted delta a world would need roughly 584 million years of simulated time to reach the overflow boundary, so the overflow rule is a correctness guarantee rather than an operational limit. Tests reach it by writing the field directly.

An accepted update advances the clock by at least one millisecond, so update_count can never exceed elapsed_time_ms and cannot overflow first. pet_world_validate now enforces that relation as a world invariant.

Rejecting a delta above one day keeps a suspended host or a wall clock jump from reaching the world, which is what SPEC-FR-016 requires. A host that observes a longer gap must decide what to do with it. Handling missed session time properly is SPEC-FR-015 and is outside this release.

Update work is constant. There is no loop over the delta, no allocation, and no deferred setup.

PetWorld grows to 64 bytes on Linux x86-64, replacing the 48 byte measurement recorded in ADR-001. That is the whole of the bound asserted by the type tests, so the next field addition requires a deliberate decision about that bound.

Using uint64_t on an 8-bit or 16-bit target costs multi-word arithmetic on every update. One addition and one increment per update is an acceptable price for a representation that never needs a wraparound rule.

Alternatives considered

A 32-bit millisecond counter. Halves the time state and is cheaper on small targets, but wraps after about 49 days of simulated time. A pet is meant to persist across long absences, so a wraparound rule would become real product behaviour rather than a theoretical boundary.

Seconds instead of milliseconds. Smaller numbers and no realistic overflow, but it forces the host to accumulate sub-second remainders, which moves a piece of timekeeping out of the Core and makes frame-driven hosts awkward.

An absolute timestamp instead of a delta. Lets the Core compute its own elapsed time, but makes the Core responsible for epochs and for clock jumps in both directions. Deltas keep clock policy in the host, where platform time belongs.

Clamping a large delta instead of rejecting it. Never fails, but silently invents a different simulation than the caller asked for. An explicit rejection lets a host tell a clock anomaly apart from a genuine absence and decide deliberately.

Splitting a large delta into fixed steps. Would let a long gap be simulated accurately, but makes update work proportional to the delta, which PBI-017 and SPEC-NFR-002 exclude.

References