Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth by AndresL230 · Pull Request #39 · recost-dev/middleware-python · GitHub
Skip to content

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth - #39

Merged
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity
May 15, 2026
Merged

Wave C-2 + C-3: wire-format cleanup, 401 lifecycle parity, flush_blocking, Twilio source-of-truth#39
AndresL230 merged 8 commits into
mainfrom
wave-c-23/parity

Conversation

@AndresL230

Copy link
Copy Markdown
Contributor

Summary

Closes#16
Closes#21
Closes#22
Closes#24
Closes#32
Closes#36

One bundled PR off origin/main. Five independent surface-level changes that overlap on the same files (_types.py, _transport.py, _init.py) so bundling avoids review queue overhead.

#36 (rolling up #16 + #22) — Wire-format cleanup

  • WindowSummary.to_dict() no longer emits projectId — the API extracts the ID from the URL path (/projects/{id}/telemetry). The dataclass field stays for in-process use.
  • New iso_now_ms_z() helper in _types.py produces millisecond-precision UTC ISO 8601 with a Z suffix. _aggregator.py switches over so windowStart / windowEnd are byte-identical to Node's new Date().toISOString().

#32 — 401 auth-failure lifecycle parity with Node

  • RecostConfig.max_consecutive_auth_failures: int = 5 (was hardcoded). Threaded through Transport via the existing config object.
  • Counter resets on EVERY non-401 outcome — 2xx success (already done), non-401 4xx (403/404/422), 5xx, network throw. Previously only reset on 2xx, so transient outages accumulated toward fatal-suspend.
  • Second distinct stderr line at fatal-suspend: [recost] cloud transport suspended after N consecutive auth failures. Restart the process after rotating apiKey.
  • First-401 stderr text now matches Node format: [recost] HTTP 401 — API key rejected. Telemetry will stop after N consecutive failures...
  • Removed dead if status == 401 branch in _report_rejection (_handle_cloud_result always handles 401 first).

#21flush_blocking() companion to dispose()

RecostHandle.flush_blocking(timeout_s: float = 3.0) -> bool runs the final flush synchronously on the calling thread. Brings Python to parity with Node's await handle.dispose() semantics for callers that need a hard ordering guarantee (short scripts, os._exit() paths, test teardown). dispose() itself is unchanged.

#24 — Twilio source-of-truth comments

Two hardcoded prices in _provider_registry.py (0.79 SMS, 1.3 Voice) now cite source URLs and a review date. Long-term plan to sync from a centralized /pricing feed is tracked by the issue.

Tests added

13 new tests:

  • tests/test_contract.py (3) — projectId absent from body, timestamps match ms+Z regex, helper format.
  • tests/test_transport.py::TestConfigurableAuthThreshold (2) — default + override.
  • tests/test_transport.py::TestAuthFailureCounterReset (3) — reset on non-401 4xx, 5xx, network throw.
  • tests/test_transport.py::TestAuthStderrText (3) — first-line text, second line at fatal-suspend, no second line below threshold.
  • tests/test_init.py::TestFlushBlocking (2) — success, timeout.
  • (drive-by Twilio pricing constants (0.79¢, 1.3¢) have no source-of-truth comment #24) Twilio comment-only change has no new test — existing test_provider_registry.py covers behavior.

Local: 210 passed, mypy clean, ruff clean.

Notable plan-vs-codebase adaptation

The plan's pseudo-code for plumbing max_consecutive_auth_failures showed adding a separate parameter to Transport.__init__. The actual constructor takes a single RecostConfig object — the implementer correctly read config.max_consecutive_auth_failures directly, which made the _init.py wiring step a no-op. Tests follow the same Transport(RecostConfig(...)) pattern.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents flush_blocking() and max_consecutive_auth_failures.

🤖 Generated with Claude Code

AndresL230and others added 8 commits May 15, 2026 02:42
…wire format (#16#22#36)
Node SDK already emits projectId via the URL path only and pins timestamps
to millisecond-precision UTC with a Z suffix. This brings Python to parity:
- WindowSummary.to_dict() no longer serializes projectId (the field stays
on the dataclass for in-process use; the API extracts the ID from the
/projects/{id}/telemetry URL path).
- New iso_now_ms_z() helper produces ms+Z timestamps. _aggregator.py
switches over in the next commit.
Adds three contract tests verifying the wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#36)
window_end now emits millisecond-precision UTC with a Z suffix, matching
Node's new Date().toISOString() output byte-for-byte. windowStart inherits
from window_end on first flush, so both are normalized in one change.
RawEvent.timestamp (internal aggregator state, never serialized to the
wire) is unchanged — only WindowSummary timestamps cross the SDK boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransport (#32)
Previously the 401-fatal-suspend threshold was hardcoded to 5. Matches
Node's RecostConfig.maxConsecutiveAuthFailures so the same numeric value
means the same thing across both SDKs.
Adds TestConfigurableAuthThreshold (2 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)
Previously _consecutive_auth_failures was only reset on 2xx success, so
transient outages (5xx, network throws, 403/404/422) accumulated toward
the fatal-suspend threshold. Matches Node's semantics: only CONSECUTIVE
401s count.
Adds TestAuthFailureCounterReset (3 tests covering non-401 4xx, 5xx,
and network throw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to Node (#32)
- First-401 stderr text now matches Node's '[recost] HTTP 401 — API key
rejected. Telemetry will stop after N consecutive failures.' format
so cross-SDK log greps find the same prefix.
- Distinct second stderr line at fatal-suspend: '[recost] cloud transport
suspended after N consecutive auth failures. Restart the process after
rotating apiKey.' Fires exactly once, on the tick that crosses the
threshold.
- Removes the dead 'if status == 401' branch in _report_rejection
(_handle_cloud_result always returns True for 401, so it was
unreachable).
Adds TestAuthStderrText (3 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h Node (#21)
Node's await handle.dispose() blocks the caller on the final flush.
Python's sync dispose() runs the flush in a daemon thread and returns
without an observable settle signal — short-lived scripts can drop the
trailing telemetry window before the thread completes.
flush_blocking() is a new method that runs the final flush on the
calling thread (bounded by timeout_s) and returns whether it completed.
Side-effect-free: does NOT mark the handle disposed, stop the periodic
timer, or close the transport. Combine with dispose() in callers that
need both ordering guarantees.
Adds TestFlushBlocking (2 tests: success and timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w date (#24)
Hard-coded 0.79¢ (SMS) and 1.3¢ (Voice) had no comment explaining where
the numbers came from or when they were last reviewed. If Twilio shifts
prices, nobody knows when to revalidate. Mirror the same fix in Node
(recost-dev/middleware-node#21).
Long-term: replace with a sync to a centralized /pricing feed. Tracked
by the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fec8bd-c519-4c38-966d-cc067e2b44ac

📥 Commits

Reviewing files that changed from the base of the PR and between 55ed1f5 and 3068644.

📒 Files selected for processing (10)
  • README.md
  • recost/_aggregator.py
  • recost/_init.py
  • recost/_provider_registry.py
  • recost/_transport.py
  • recost/_types.py
  • tests/test_contract.py
  • tests/test_init.py
  • tests/test_transport.py
  • tests/test_types.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-c-23/parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230
AndresL230 merged commit e4bf61f into mainMay 15, 2026
3 of 5 checks passed
@AndresL230
AndresL230 deleted the wave-c-23/parity branch May 21, 2026 04:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment