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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ the GitHub Release body, so a release with no entry here fails.

Versioning follows [docs/versioning.md](docs/versioning.md).

## [0.9.1] - 2026-09-08

### Fixed

- `new_event_id()` now guarantees that its opaque 16-character lowercase-hex
value contains an alphabetic namespace marker. Previously it returned the
first 16 characters of a UUID4. Because the UUID version nibble is always the
decimal digit `4`, the whole value was accidentally decimal with probability
`(10/16)^15` (about 1 in 1,153). `AgentComm.event_ordinal()` then accepted that
opaque id as the legacy decimal-store fallback and advanced its in-process
cursor to a huge, non-monotonic value instead of raising
`EventStoreContractError`. The new shape remains 16 lowercase hex characters
and retains the same 60 random bits, but its namespace can no longer overlap
decimal store ordinals. A deterministic all-decimal UUID-source regression
test replaces the probabilistic coverage that made the contract test flaky.

## [0.9.0] - 2026-09-06

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion agent_core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ def new_task_id() -> TaskId:


def new_event_id() -> EventId:
return EventId(uuid4().hex[:16])
raw = uuid4().hex
# Keep the historical 16-character lowercase-hex shape while making the
# opaque-id namespace disjoint from decimal store ordinals. UUID4's
# version nibble at index 12 is fixed to ``4``; replacing that non-random
# nibble with an alphabetic marker preserves the original 60 random bits
# and guarantees ``event_ordinal()`` cannot mistake this id for a cursor.
return EventId(f"e{raw[:12]}{raw[13:16]}")


def new_session_id() -> SessionId:
Expand Down
5 changes: 3 additions & 2 deletions docs/agent-bus-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ retained event: `KernelEvent.seq`, a monotonically increasing integer, or a
decimal `KernelEvent.id` for stores whose integer primary key already serves as
the id. `EventReader`'s `after_id` cursor is expressed in that ordinal, and it —
not `timestamp` — defines the order. `EventId` is deliberately an opaque
string, so `types.new_event_id()` (uuid4 hex: neither numeric nor monotonic)
cannot order a cursor; a store that stamps only that fails
string, so `types.new_event_id()` (16-character uuid4-derived hex with a
guaranteed alphabetic namespace marker: neither numeric nor monotonic) cannot
order a cursor; a store that stamps only that fails
`AgentComm.consume` with `EventStoreContractError`, and `AgentBus` logs the
lost durable-recovery capability at ERROR while still degrading to the
in-memory path. `tests/test_agent_comm_store_contract.py` is the executable
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "apodex-agent-core"
version = "0.9.0"
version = "0.9.1"
description = "Shared, product-neutral runtime primitives for Apodex agents"
readme = "README.md"
license = "Apache-2.0"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_agent_comm_store_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import pytest

from agent_core import types
from agent_core.components.agent_bus.agent_comm import (
AgentComm,
DeliveryMode,
Expand Down Expand Up @@ -234,6 +235,25 @@ def test_default_event_id_generator_cannot_order_a_cursor():
event_ordinal(event)


def test_all_decimal_uuid_source_still_produces_an_opaque_event_id(monkeypatch):
"""The opaque-id namespace must not overlap decimal store ordinals."""

class _AllDecimalUUID:
hex = "12345678901234567890123456789012"

monkeypatch.setattr(types, "uuid4", _AllDecimalUUID)

event_id = types.new_event_id()

assert event_id == "e123456789012456"
assert not event_id.isdecimal()
event = KernelEvent(
task_id=TaskId("t1"), event_type=EventType.AGENT_MESSAGE, id=event_id,
)
with pytest.raises(EventStoreContractError):
event_ordinal(event)


# ──────────────────────────────────────────────────────────────────────────
# The store protocol: which shape of ``append`` AgentComm requires
# ──────────────────────────────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion tests/test_runtime_foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@


def test_identity_shapes_and_string_event_contract() -> None:
assert len(new_event_id()) == 16
event_id = new_event_id()
assert len(event_id) == 16
assert not event_id.isdecimal()
assert len(new_session_id()) == 12
assert len(new_prompt_id()) == 12
assert len(new_step_id()) == 10
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.