Skip to content

epic-5/story-2: Add Post-MVP Automatic Config Refresh - #44

Closed
usmanabbas7 wants to merge 9 commits into
epic-5/story-1-establish-maintainer-release-and-parity-validation-workflowfrom
epic-5/story-2-add-post-mvp-automatic-config-refresh
Closed

epic-5/story-2: Add Post-MVP Automatic Config Refresh#44
usmanabbas7 wants to merge 9 commits into
epic-5/story-1-establish-maintainer-release-and-parity-validation-workflowfrom
epic-5/story-2-add-post-mvp-automatic-config-refresh

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Story 5.2 — Add Post-MVP Automatic Config Refresh

Part of sprint sprint/2026-04-06-convert-python-sdk. Stacked on epic-5/story-1 (PR #43 → … → Epic 3 chain).

Implements FR31 (post-MVP, opt-in automatic config refresh). MVP behavior is byte-for-byte unchanged when refresh=None (the default).

What was built

  • RefreshConfig (frozen dataclass) on SDKConfig.refresh: Optional[RefreshConfig] = None, with __post_init__ validation. Opt-in only — default spins up no daemon thread, emits no events, adds no cost.
  • ConfigRefresher daemon-thread worker (config_loader/refresh.py) — reuses the existing Story 1.2 Transport Protocol and the existing load_snapshot pipeline (no parallel transport). Interruptible sleep, deterministic test seams (no wall-clock timing in tests).
  • Atomic snapshot swap on Core (mutex-protected) so in-flight evaluations always see a single coherent snapshot; long-lived Context objects retain their creation-time snapshot. Core.refresh_now(), Core.close(), and context-manager protocol added.
  • Failure handling (AC-3): transient failures back off; the prior good snapshot keeps serving; failures surface via Story 4.1's diagnostic logger + Story 4.2's typed-error contract; background failures never crash the host process. refresh.start/success/skipped/fail/worker_crashed diagnostics.
  • JS parity: TrackingQueue.update_snapshot_metadata() re-points account/project IDs on swap; LifecycleEvent.CONFIG_UPDATED fires on each successful refresh.
  • Direct-config guard: RefreshConfig + direct config emits refresh.skipped instead of silently ignoring.
  • Docs: docs/initialization.md refresh-policy section; docs/migration-from-javascript.md unit/default-divergence mapping; towncrier fragment.

ADR gate (audit F-027 + F-028)

docs/adr/0001-config-refresh-concurrency-and-backoff.md ratifies the mutex-protected swap (atomic w.r.t. an evaluating caller, safe under free-threaded CPython 3.13+ — not relying on GIL atomicity) and every concrete backoff/interval/jitter value (interval 300s, jitter 30s, factor 2.0, max 600s).

Audit findings addressed

  • F-032 — AC-3 (background-failure resilience) retained and fully implemented.
  • F-027 — atomic swap; concurrency mechanism ratified in ADR 0001.
  • F-028 — backoff numeric values ratified in ADR 0001.
  • F-053 — Task 5 docs trace to FR31 (not FR54).

Tests

762 tests pass (was 732; +30 in tests/test_config_refresh.py). scripts/verify_release.py all gates green (ruff, mypy --strict, coverage ≥85% / evaluation ≥95%, parity suite, towncrier draft, uv build).

Beads

epic ai-driven-product-dev-14vo; tasks -9hwq / -577c / -usaq / -eugx / -5r84 (all closed).

Sprint-mode notes

  • Readiness gate: composite 8.6/10 PASS round 1; 5 ambiguities (clock seam, lifecycle placement, CONFIG_UPDATED reconciliation, F-027 concurrency, F-028 backoff) auto-delegated "your call" in sprint mode and resolved in the conductor's assessment report.
  • Code review: passed (round 1 fixed a stop() self-join deadlock guard + worker_crashed coverage; round 2 clean).

🤖 Generated with Claude Code

@usmanabbas7 usmanabbas7 self-assigned this Jun 8, 2026
@usmanabbas7
usmanabbas7 requested a review from clllaur June 8, 2026 14:02

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements an opt-in automatic config refresh feature using a background daemon thread with mutex-protected atomic snapshot swaps, exponential backoff, and lifecycle event emissions. The review feedback highlights critical resilience issues, specifically potential OverflowError crashes during backoff calculations under persistent failures, and a race condition in the thread shutdown logic that could spawn duplicate active daemon threads.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +272 to +275
backed_off = self._policy.interval_seconds * (
self._policy.backoff_factor ** self._consecutive_failures
)
return backed_off >= self._policy.backoff_max_seconds

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Under persistent network or endpoint failures, self._consecutive_failures will grow indefinitely. When calculating self._policy.backoff_factor ** self._consecutive_failures, this will eventually raise an OverflowError (e.g., 2.0 ** 1024 overflows in Python), crashing the background daemon thread and preventing any future retries. Catching OverflowError ensures the worker remains resilient and correctly caps the backoff at backoff_max_seconds.

        try:
            backed_off = self._policy.interval_seconds * (
                self._policy.backoff_factor ** self._consecutive_failures
            )
            return backed_off >= self._policy.backoff_max_seconds
        except OverflowError:
            return True

Comment on lines +284 to +286
if self._consecutive_failures > 0:
backed_off = base * (self._policy.backoff_factor ** self._consecutive_failures)
base = min(backed_off, self._policy.backoff_max_seconds)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similarly to _at_terminal_backoff, calculating the exponential backoff here can raise an OverflowError if self._consecutive_failures grows large. Catching OverflowError and falling back to backoff_max_seconds prevents the daemon thread from crashing.

        if self._consecutive_failures > 0:
            try:
                backed_off = base * (self._policy.backoff_factor ** self._consecutive_failures)
                base = min(backed_off, self._policy.backoff_max_seconds)
            except OverflowError:
                base = self._policy.backoff_max_seconds

Comment on lines +138 to +145
thread = self._thread
if (
thread is not None
and thread.is_alive()
and thread is not threading.current_thread()
):
thread.join(timeout=timeout)
self._thread = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Setting self._thread = None at the end of stop() before the thread has actually exited (or when called re-entrantly from the worker thread itself where join() is skipped) creates a race condition. If start() is called immediately after, it will see self._thread as None, clear self._stopping, and spawn a new thread. The old thread, seeing self._stopping cleared, will continue running, resulting in duplicate active daemon threads. Keeping the reference to self._thread if it is still alive prevents start() from clearing self._stopping and spawning a duplicate thread.

Suggested change
thread = self._thread
if (
thread is not None
and thread.is_alive()
and thread is not threading.current_thread()
):
thread.join(timeout=timeout)
self._thread = None
thread = self._thread
if (
thread is not None
and thread.is_alive()
and thread is not threading.current_thread()
):
thread.join(timeout=timeout)
if not thread.is_alive():
self._thread = None

@usmanabbas7
usmanabbas7 force-pushed the epic-5/story-1-establish-maintainer-release-and-parity-validation-workflow branch from 13f7556 to 87786af Compare June 14, 2026 17:13
@usmanabbas7
usmanabbas7 force-pushed the epic-5/story-2-add-post-mvp-automatic-config-refresh branch from b99ceaa to 65349c2 Compare June 14, 2026 17:15
@usmanabbas7
usmanabbas7 force-pushed the epic-5/story-1-establish-maintainer-release-and-parity-validation-workflow branch from 87786af to 331edf0 Compare June 15, 2026 12:03
usmanabbas7 and others added 9 commits June 15, 2026 17:08
Beads: ai-driven-product-dev-9hwq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-9hwq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-577c

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…— implementation (GREEN)

Beads: ai-driven-product-dev-577c

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…t — tests (RED)

Beads: ai-driven-product-dev-usaq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…king re-point — implementation (GREEN)

Beads: ai-driven-product-dev-usaq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-eugx

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…rier fragment

Beads: ai-driven-product-dev-5r84

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…shed coverage)

- stop() skips self-join when called from worker thread (deadlock guard)
- remove pragma; test refresh.worker_crashed guard + re-entrant stop

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@usmanabbas7
usmanabbas7 force-pushed the epic-5/story-2-add-post-mvp-automatic-config-refresh branch from 65349c2 to 8a6badc Compare June 15, 2026 12:08
@usmanabbas7

Copy link
Copy Markdown
Collaborator Author

F-066 propagation (rebase onto remediated 3-3)

Rebased --onto the remediated 5-1 (dropping the now-duplicated [REL-1] parent commit). This branch's own commits do not touch evaluation/segments.py, so the F-066 latch is inherited cleanly — segments.py byte-identical to remediated 3-3, two latch parity tests present. No conflicts.

  • uv run pytest772 passed · uv run ruff check clean · uv run mypy --strict clean (44 files)
  • CI gate: PASSED — all 21 checks green (ruff, mypy --strict, changelog, build, bounds-check, full py3.9–3.13 × {ubuntu,macos,windows} matrix).

@abbaseya

Copy link
Copy Markdown
Collaborator

Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup.

@abbaseya abbaseya closed this Jun 18, 2026
@abbaseya
abbaseya deleted the epic-5/story-2-add-post-mvp-automatic-config-refresh branch June 18, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants