You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Addresses all four review findings:
1. [P1] Replay isolated by credentials: journal records now live in a
per-identity subdirectory keyed by a non-secret truncated SHA-256
fingerprint of (base_url, api_key). Co-located clients with different
servers or API keys can no longer replay each other's records — a
foreign record previously got 401/403 and was discarded as terminal,
permanently losing the spend.
2. [P1] HTTP 429 / LIMIT_EXCEEDED is transient, not terminal: both the
commit and event classifiers now detect rate limiting (status 429 or
error code), retain the journal entry, keep retrying, and make the
next attempt wait at least the server's Retry-After (consumed once,
max'd against the normal backoff). Consistent with
ErrorCode.is_retryable, which already classified LIMIT_EXCEEDED
retryable.
3. [P2] Replay claim scoped to the identity subdirectory: because the
claim now covers exactly one (server, credential) identity, an engine
for server A can never block server B's entries from replaying out of
a shared journal_dir.
4. [P2] Process-wide flush deadline: _flush_all_engines() computes one
deadline from the max engine timeout and passes each engine only the
remaining budget, so worst-case shutdown is retry_flush_timeout, not
engine_count x retry_flush_timeout.
No journal-layout migration needed — v0.5.0 is unreleased.
469 tests pass at 100% coverage; ruff and mypy --strict clean.
AUDIT.md, CHANGELOG.md, README.md updated.
Copy file name to clipboardExpand all lines: AUDIT.md
+9-2Lines changed: 9 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Cycles Protocol v0.1.25 — Client (Python) Audit
2
2
3
-
**Date:** 2026-07-27 (v0.5.0 — durable commit retries: on-disk pending-commit journal with next-run replay, bounded atexit flush, and `POST /v1/events` recovery for commits that land after reservation expiry; async retry-task GC fix; `retry_enabled=False` now journals instead of silently dropping. See the dated entry below. 460 tests pass at 100% coverage.),
3
+
**Date:** 2026-07-27 (v0.5.0 — durable commit retries: on-disk pending-commit journal with next-run replay, bounded atexit flush, and `POST /v1/events` recovery for commits that land after reservation expiry; async retry-task GC fix; `retry_enabled=False` now journals instead of silently dropping. Review hardening: per-identity journal partitioning, 429 transient with `Retry-After`, process-wide flush deadline. See the dated entry below. 469 tests pass at 100% coverage.),
4
4
2026-07-10 (v0.5.0 — `TENANT_CLOSED` + `LIMIT_EXCEEDED` error-code support. `TENANT_CLOSED` per runtime spec v0.1.25.13 (`cycles-protocol-v0.yaml`, runcycles/cycles-protocol#125): `ErrorCode.TENANT_CLOSED` enum member, `TenantClosedError` subclass wired into the lifecycle error-code→exception mapping (reservation-creation surfaces), `CyclesProtocolError.is_tenant_closed()` helper. `LIMIT_EXCEEDED` per runtime spec v0.1.25.12 (revision 2026-07-04, HTTP 429 rate limiting): enum-only member matching the `BUDGET_FROZEN`/`BUDGET_CLOSED` pattern, classified retryable at both the enum and exception layers (429 is transient; previously it fell through to `UNKNOWN`, which happened to be retryable, so semantics are unchanged — now typed). Enum reordered to mirror spec declaration order. Both purely additive; previously both codes fell through the `ErrorCode.from_string` forward-compat path to `UNKNOWN`. See the dated entries at the end of this file. 398 tests pass at 100% coverage.),
5
5
2026-07-09 (README + docstring transport-error documentation fix, no version bump — see the dated entry at the end of this file. `CyclesTransportError` is exported but never raised by the SDK; README and its docstring now describe the actual `status == -1` surfacing.),
6
6
2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.),
@@ -23,7 +23,14 @@ has already returned the reserved budget to the pool — is recovered via
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,9 +11,10 @@ Durable commit retries. Previously a commit that failed transiently lived only i
11
11
12
12
### Added
13
13
14
-
-`runcycles.journal`: file-per-commit `CommitJournal` (atomic write, idempotent replay). Config: `journal_enabled` (default `True`), `journal_dir` (default `~/.runcycles/commit-journal`), `retry_flush_timeout` (default 10 s); env `CYCLES_JOURNAL_ENABLED`, `CYCLES_JOURNAL_DIR`, `CYCLES_RETRY_FLUSH_TIMEOUT`. The first engine created per journal directory replays surviving entries for its `base_url`; corrupt files are renamed `*.corrupt` for operator triage.
14
+
-`runcycles.journal`: file-per-commit `CommitJournal` (atomic write, idempotent replay). Config: `journal_enabled` (default `True`), `journal_dir` (default `~/.runcycles/commit-journal`), `retry_flush_timeout` (default 10 s); env `CYCLES_JOURNAL_ENABLED`, `CYCLES_JOURNAL_DIR`, `CYCLES_RETRY_FLUSH_TIMEOUT`. Records are partitioned into per-identity subdirectories keyed by a non-secret SHA-256 fingerprint of `(base_url, api_key)`, so clients with different servers or credentials sharing a journal directory never replay — or 401-discard — each other's records, and one identity's replay claim cannot starve another's. The first engine created per identity replays surviving entries; corrupt files are renamed `*.corrupt` for operator triage.
15
15
- Event fallback: when a commit (first attempt or retry) returns `RESERVATION_EXPIRED`, the SDK posts the spend to `/v1/events` reusing the commit's idempotency key, with `metadata.recovered_reservation_id` / `metadata.recovery_reason` markers and no `overage_policy` (spec default `ALLOW_IF_AVAILABLE` never rejects). Applies to the `@cycles` lifecycles and both streaming context managers. `RESERVATION_FINALIZED` is still treated as settled.
16
-
-`flush()` on both retry engines; a process-wide `atexit` hook flushes sync engines for up to `retry_flush_timeout` seconds so daemon retry threads aren't killed mid-backoff on clean exit.
16
+
-`flush()` on both retry engines; a process-wide `atexit` hook flushes sync engines under one shared `retry_flush_timeout` deadline (not per engine) so daemon retry threads aren't killed mid-backoff on clean exit and shutdown time stays bounded regardless of engine count.
17
+
- Rate-limit awareness in the retry engines: HTTP 429 / `LIMIT_EXCEEDED` on a commit or event attempt is transient — the journal entry is retained and the next attempt waits at least the server's `Retry-After` (consistent with `ErrorCode.is_retryable`).
0 commit comments