withCyclesno longer releases a reservation after a recognized terminal commit rejection. The guarded function has already spent the resource, so returning its reserved budget would undercount known spend.StreamReservation.commit()still surfaces a recognized terminal rejection, but keeps the handle finalized. A broad caller catch can no longer turn the failed settlement into a release of known spend.withCyclesnow releases only when the guarded function itself fails. Post-action settlement/setup failures never return budget for work that already ran; a failing or invalidactualcallback falls back to the validated estimate and recordsmetadata.actual_source="estimate".- A configuration that disables estimate fallback without providing
actualis rejected before a reservation is created or the guarded function runs. StreamReservation.commit()now replaces a non-finite, negative, fractional, or unsafe-integer actual with the validated estimate and anactual_source="estimate"marker instead of journaling an invalid amount.
- Regression tests pin no-release behavior for lifecycle, post-action, and streaming paths.
- README settlement tables and error guidance now distinguish handler failure (release) from post-action commit rejection (never release).
- The development lockfile updates
brace-expansionto 5.0.9, clearing the current high-severity expansion DoS advisories; published runtime dependencies are unchanged.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog 1.1.0.
- Heartbeat transport exceptions from both
withCyclesandreserveForStreamare now reported with the reservation ID, error detail, and same-key retry disposition. Heartbeats remain non-fatal and retain their existing recovery timing, but a lost extend response is no longer invisible to operators. - Known actual usage is journaled before the first commit request. Commit and event settlement now require exact schema-valid HTTP 200/201 success; malformed 2xx responses retain the original idempotency key and durable record.
- Journal filenames now use
v2-<sha256(exact UTF-8 reservation id)>.json, with collision-safe legacy migration. Contradictory retryable 4xx envelopes retain the record. - Pull-request and release CI now run every shared durable-recovery and guarantee-boundary scenario; publishing is gated on conformance.
CommitResponseand its wire mapper now expose the optionalcycles_evidencereference.- Unsupported or structurally invalid journal records are quarantined without blocking valid replay, and the conformance adapter reports the exact native test it executed instead of copying runner-owned oracle outcomes.
- Final lease-response and timing conformance (supersedes the older fallback wording below). Both server-authoritative and fieldless fallback scheduling count only a complete, schema-valid HTTP 200 create/extend response as success; malformed or non-200 2xx responses remain ambiguous and are recovered with the same idempotency key. Create performs one same-key recovery attempt, the enforced timeout covers the whole attempt, post-receipt setup time is deducted from the first delay, and reliable RTT samples gathered before a rolling-upgrade server starts sending
remaining_ttl_msremain part of the safety budget. - Heartbeat extend drift (P1 liveness). Per the protocol spec,
extend_by_msextends relative to the currentexpires_at_ms, and the server capsextension_countatmax_extensions(default 10). Both heartbeats (withCyclesandreserveForStream) beat everymax(ttlMs/2, 1000)ms and extended by the fullttlMson every beat, so expiry drifted outward byttl/2per beat — a process crash left the budget reserved until the drifted expiry (zombie budget lockup) — and the extension budget burned twice as fast as needed, so runs longer than aboutmax_extensions * ttl/2lost heartbeat protection mid-flight. Both heartbeats now use measured-grant lead accounting (v2.3): each beat computes a rigorous lower bound on the expiry lead,leadMin = grantsSum − (now − anchor), where grants are differences of successiveexpires_at_msvalues returned by the server (same server clock frame; a 2xx without a numericexpires_at_mscounts as the requested ttl; negative grants count as 0) and the elapsed term is client-monotonic — no cross-clock arithmetic anywhere, andleadMinstarts at 0 because the initial grant has no safe same-clock anchor to measure against (see the next entry), so the bound never overstates the real lead. A beat is skipped only whenleadMin ≥ 1.5 × lastGrant(the last measured grant); otherwise it extends by the requestedttl_ms— the server owns clamping. On the fallback path (create response withoutremaining_ttl_ms— see the round-5 entry under Added) the first beat fires immediately (delay 0): spec review round 4 confirmed that any bounded first-beat delay can outlive a small capped lease (tenant policymax_reservation_ttl_mscaps grants silently and the create response then carries no effective-TTL signal), so the first extend primes the lease with a real, measurable grant right away instead of gambling on an unknowable one. The 0 delay applies to the first schedule only — a transiently failed first attempt retries after the full held intervalmin(requestedTtl/2, 30 s)with the same idempotency key, never hot-loops. After each applied grant the cadence re-derives per a regime split (round 4): grant-derived cadence is only valid when the grant is a real per-extend amount. Under a maximum-lead clamp (the server holdsexpires_at_ms ≈ now + L), successiveexpires_at_msdifferences measure elapsed time, not granted lease — deriving the cadence from them would collapse the interval to the 500 ms floor and burnmax_extensionsin seconds. A grant that is non-positive, or both below0.9 ×the requested ttl and within the0.75 ×–1.25 ×band of the elapsed time since the last applied extend, is therefore treated as lead-clamped: the cadence holds atmin(requestedTtl/2, 30 s), a once-per-heartbeatconsole.warnreports that the server appears to clamp lease lead and the extension budget will deplete, and the heartbeat keeps extending every beat (lastGrant ≈ elapsedkeepsleadMinbelow the skip threshold — exactly the desired liveness behavior). The band must be two-sided (Rust-port finding, adopted fleet-wide): after aleadMinskip the next grant arrives across a doubled gap, so a genuine grant-clamped server (cadencegrant/2) also showsgrant ≈ elapsedexactly once — an upper bound alone would classify it as lead-clamped and the hold would self-sustain (at the held cadence the grant stays≤elapsed forever, decaying the lease to a lapse: a 15 s-per-extend lease banks only +15 s per 30 s of held-cadence wall time). With the band, a real maximum-lead clamp tracks any gap with ratio ≈ 1 and stays held, while the post-skip real grant lands in the hold once: at the held cadence its ratio falls to ≈ 0.5, exits the band, and the cadence re-tightens. Round 5 then established that regime detection from(grant, elapsed)alone is formally undecidable — any per-extend grant in the sticky window[0.75 × min(ttl/2, 30 s), 0.9 × ttl)reproduces the elapsed gap at the held cadence and misclassifies permanently (e.g. ttl 24 s with +10 s grants: held cadence 12 s, ratio 10/12 ≈ 0.83 sits inside the band forever while the lease erodes to a lapse) — so this heuristic is a best-effort fallback for servers that clamp only the per-extend delta; the server-authoritativeremaining_ttl_ms(see Added) is the normative path. A real per-extend grant re-derives the cadence asclamp(grant/2, 500 ms, requestedTtl/2)— a server that clamps the grant size automatically tightens the cadence (24 h request granted 1 h per extend → 30 min beats), with no false skips because the skip threshold scales with the same measured grant. The 500 ms interval floor cannot starve liveness: it binds only when the server grants less than the spec's own minimumttl_msper extend. A failed extend is retried on the next beat at the current cadence reusing the same idempotency key, so a lost response cannot double-extend; a success regenerates the key. Permanent rejections — HTTP 410,RESERVATION_EXPIRED,RESERVATION_FINALIZED,MAX_EXTENSIONS_EXCEEDED,TENANT_CLOSED(tenant closure is irreversible), orNOT_FOUND(a 404'd reservation never comes back) — now stop the heartbeat for good (previously it retried doomed extends forever); transient failures log a warning and keep retrying.ctx.expiresAtMsis still updated from the authoritative extend response. (Replaces the alternate-beat scheme first shipped on this branch — adversarial review showed it put every steady-state attempt at exactlyttl/2lead and its 1 s interval floor guaranteed a lapse for spec-legalttl < 2000— the lead-estimate scheme that followed it, which counted an unmeasurable initial+ttlof lead and leaned on the HTTPDateheader for correctness, and the v2.2 revision, which kept aDate-hinted bounded first-beat delay and let elapsed-sized grants tighten the cadence.) - HTTP
Dateheader fully demoted out of the heartbeat (spec review rounds 3–4). An earlier revision of this branch recovered an "effective TTL" asexpires_at_ms − Date(viaCyclesResponse.serverDateMs) and used it for the beat interval, the lead arithmetic, the skip threshold, andextend_by_ms; round 3 demoted it to a first-beat cadence hint. TheDateheader is not a safe same-clock anchor forexpires_at_ms: RFC 9110 definesDateas a whole-second, best-effort origination timestamp that intermediaries may generate or replace, and in the reference serverexpires_at_mscomes from RedisTIMEwhileDatecomes from the servlet container — two different clocks. Round 4's immediate first beat removes the last consumer of the hint: the first measured grant now arrives at ~t=0, strictly better than any estimate, socomputeEffectiveTtlMsand its plumbing are deleted outright.CyclesResponse.serverDateMsremains as a general-purpose response accessor; the heartbeat no longer reads it. Every quantity in the heartbeat — first-beat timing, lead accounting, skip threshold, cadence, andextend_by_ms— derives from measured grants, monotonic elapsed time, or the requested ttl. Never fromDate.
- Server-authoritative heartbeat scheduling via
remaining_ttl_ms(spec PR #148, settled at headdd60c27). The protocol addsremaining_ttl_ms(integer, int64, ≥ 0) to bothReservationCreateResponseandReservationExtendResponse: the remaining reservation lifetime in ms at response evaluation, in the same clock snapshot asexpires_at_ms, present on successful live-reservation responses (absent on dry-run/DENY and on older servers;cycles-serveris implementing emission in parallel). Both heartbeats implement the spec's PRIMARY ALGORITHM, normative whenever the field is present. Success predicate: only a schema-valid HTTP 200ReservationExtendResponse(status: "ACTIVE", integerexpires_at_ms ≥ 0, optional integerremaining_ttl_ms ≥ 0) counts as an observed success on this path; any other or malformed 2xx is ambiguous — never used to schedule from stale state — and is recovered like a transient failure with the same idempotency key. Scheduling, recomputed from every schema-valid response alone (expiry differences are never accumulated, and the heuristicleadMinskip check is bypassed — the schedule is exact, so a heuristic skip could push a beat past the real lease):rttis each individual HTTP attempt's monotonicresponse_received − attempt_sent(max tracked per heartbeat);lead_floor = max(0, remaining_ttl_ms − rtt);request_timeout_budgetis the client's enforced finite per-attempt bound (connectTimeout + readTimeout, whichCyclesClientapplies to every request viaAbortSignal.timeout);attempt_budget = max(request_timeout_budget, 1 s, 2 × maxRtt);safety_margin = max(1 s, 2 × maxRtt);retry_reserve = 2 × attempt_budget + safety_margin;next_delay = max(0, lead_floor − retry_reserve)after response receipt. Budgets and margins round up, leads and delays round down; arithmetic is overflow-safe (saturating to Infinity — an unknown or unbounded timeout makesattempt_budgetinfinite andnext_delay0). Zero-delay guard: a schema-valid success producingnext_delay = 0permits exactly one immediate fresh attempt (new idempotency key); if that success also yields 0, the heartbeat stops and surfaces that the lease is shorter than the retry-safety budget. Unavailable/unreliable attempt timing forceslead_floor = 0into the same guard — never a silent downgrade to the fieldless fallback. Recovery (timeout, connection error, 5xx, 429, ambiguous 2xx):current_lead_estimate = max(0, last lead_floor − monotonic elapsed since the schema-valid response that established it);retry_window = current_lead_estimate − attempt_budget − safety_margin(unclamped). A negative window means no complete retry plus margin provably fits: stop and surface. Otherwise non-429 failures retry with the same key aftermin(30 s, current_lead_estimate/4, retry_window); 429 convertsRetry-Afterdelta-seconds to ms (overflow-safe) and retries after exactly that delay only when it fits the window — missing, invalid, or window-exceeding values stop and surface (never an earlier retry that violates throttling). Repeated recovery is allowed: every failed or ambiguous attempt recomputes lead and window from the same last schema-valid response before deciding again; a zero window permits one immediate retry, and a progress guard stops the loop if neither elapsed time nor the window moves between consecutive failures. Any other 4xx stops and surfaces without ever rotating the idempotency key on an unchanged request; the permanent-stop codes are unchanged. First beat: derived from the create response'sremaining_ttl_mswith the same formula, using the create call's own measured rtt — no immediate prime, so nomax_extensionsslot is wasted under a max-lead clamp (same-key create/extend replays are safe to schedule from: the server recomputesremaining_ttl_msat replay-response construction time). The grants/lead bookkeeping keeps running underneath, so if the field disappears mid-flight (proxy strips it, mixed-version fleet) the v2.3 heuristic takes over seamlessly; servers that never send it get the unchanged fallback behavior. metadata.actual_source = "estimate"marker on estimate-fallback commits. Whencfg.actualis not configured anduseEstimateIfActualNotProvidedis left at its default,withCyclescommits the estimate as the actual; previously that estimate was recorded as measured spend with no trace. The commit body now carriesactual_source: "estimate"inmetadata(merged with any commit metadata set viactx.commitMetadata; created when absent) plus aconsole.debugnote, and the marker flows into thePOST /v1/eventsfallback body, which copies commit metadata. The default fallback behavior itself is unchanged.reserveForStreamis unaffected:handle.commit(actual, ...)always takes an explicit caller-provided actual, so no fallback (and no marker) exists there.
Durable commit retries. Previously a commit that failed transiently lived only in a floating in-memory promise: process.exit(), a crash, or a signal dropped it, and once the reservation's grace period elapsed the server's expiry sweep returned the reserved budget to the pool, permanently under-counting spend that had already happened. Ports the full design from cycles-client-python v0.5.0 (PR runcycles/cycles-client-python#89, three review rounds).
src/journal.ts: file-per-commitCommitJournal(atomic unique-temp-file write, idempotent replay). Config:journalEnabled(defaulttrue),journalDir(default~/.runcycles/commit-journal),retryFlushTimeout(default 10 s); envCYCLES_JOURNAL_ENABLED,CYCLES_JOURNAL_DIR,CYCLES_RETRY_FLUSH_TIMEOUT. Records are partitioned into per-identity subdirectories (directories0700, files0600where supported) keyed by a non-secret PBKDF2-HMAC-SHA256 fingerprint of the server plus principal — the configuredtenantwhen set (rotation-safe), else the API key. The derivation is byte-compatible with the Python SDK, so same-tenant clients in both languages share an identity directory and can settle each other's records. The first engine created per identity replays surviving entries; corrupt files are renamed*.corrupt; a persistednot_before_msfloor makesRetry-Afterwaits survive restarts.- Event fallback: a commit answered
RESERVATION_EXPIRED(budget already returned to the pool) is recovered viaPOST /v1/events, reusing the commit idempotency key withmetadata.recovered_reservation_id/recovery_reasonmarkers and nooverage_policy(spec defaultALLOW_IF_AVAILABLEnever rejects). Applies towithCyclesand the streaming adapter. - Rate-limit awareness end to end: 429 /
LIMIT_EXCEEDEDon the first commit attempt schedules a retry instead of releasing the reservation, passing the server'sRetry-Afterinto the engine; on retried attempts the journal entry is retained and the next attempt waits at leastRetry-After. - Authentication failures (401/403) on any commit attempt or event fallback journal the spend instead of releasing or discarding it.
CommitRetryEngine.scheduleEvent()andflush(timeoutMs?).flushPendingCommits(timeoutMs?)— public, exported from the package root: waits (bounded) for all in-flight background commit retries across every engine in the process, including the engineswithCyclesandreserveForStreamcreate internally. Defaults to the maximumretryFlushTimeoutamong engines. Call it before returning a handler response in serverless environments.
StreamReservation.commit()no longer throws on transient failures. Transport errors, 5xx, 429, 401/403, and post-expiry commits are journaled and retried in the background (with the/v1/eventsfallback once expired) and resolve normally withfinalizedremainingtrue. Only genuine rejections (e.g.UNIT_MISMATCH) still resetfinalizedand throw so the caller can correct and retry or release. Previously every failure threw and resetfinalized, leaving spend recovery entirely to the caller.- Retry-engine promises are tracked (awaitable via
flush()) instead of floating; retries that exhaust or fail non-retryably retain their journal entry (transient/auth) or discard it (genuine rejection) instead of silently dropping the spend record. - With
retryEnabled: false, failed commits are journaled for next-run replay instead of silently dropped (the old drop behavior remains only when the journal is also disabled). - Unclassifiable 4xx commit responses no longer release or discard spend. A 4xx is treated as a genuine rejection (release in
withCycles, throw inStreamReservation.commit(), journal discard in the retry engine) only when it carries a recognized protocol error code; codeless, mangled, or forward-compat unknown codes are journaled and retained with an error log instead. HTTP 410 by status alone is now classified asRESERVATION_EXPIRED(catches bodyless 410s) and recovered via the/v1/eventsfallback. - Honored server delays are clamped to 1 hour (
Retry-Afterpassed toschedule(), stashed from a 429, or a restored journalnot_before_msfloor) — a mangled header or corrupted timestamp cannot park a spend record for days or overflow Node's 2^31-1 mssetTimeoutlimit. - Cross-SDK journal parse strictness (Python/Java parity): records with
mode: nullor array-valuedcommit_body/event_fallback_bodyare quarantined as*.corruptinstead of coerced; an emptyevent_fallback_bodyon an expired commit is treated as absent (journal retained, no empty/v1/eventspost). A whitespace-only configuredtenantnow falls back to the API key as the journal identity principal (the raw untrimmed tenant is still used when non-blank). journal.record()also tightens permissions (0700, best-effort) on the base journal directory, andloadPending()garbage-collects*.tmpfiles older than one hour left behind by crashed writers.
Protocol error handling, response-mapping correctness, and release-pipeline hardening. This is the first published release after 0.3.1; the changes previously documented as 0.3.2 and 0.3.3 are included here because those versions were never tagged or published.
TENANT_CLOSEDerror-code support introduced in runtime spec v0.1.25.13 (runcycles/cycles-protocol#125): newErrorCode.TENANT_CLOSEDenum member,TenantClosedErrorclass (thrown at reservation time bywithCycles/ lifecycle /reserveForStreamviabuildProtocolException; commit-time client errors are handled/released internally bywithCycles, andStreamReservation.commit()throws genericCyclesError), andCyclesProtocolError.isTenantClosed()helper. The code is non-retryable and remains backward compatible with servers that return unknown future error codes.LIMIT_EXCEEDEDerror-code support per runtime spec v0.1.25.12 (revision 2026-07-04): HTTP 429 rate-limit responses (public evidence/JWKS endpoints) carryerror=LIMIT_EXCEEDEDplusRetry-After/X-RateLimit-Resetheaders. NewErrorCode.LIMIT_EXCEEDEDenum member in spec declaration order (afterMAX_EXTENSIONS_EXCEEDED;TENANT_CLOSEDrelocated after it so the enum mirrors the spec exactly). Classified retryable by bothisRetryableErrorCodeandCyclesProtocolError.isRetryable()— 429 is transient and the spec instructs retry after the indicated delay; the status-based rule only covers ≥500, so the code-based classification carries it (this also preserves the priorerrorCodeFromString → UNKNOWN → retryablefallback behavior). Enum-only by design, matching theBUDGET_FROZEN/BUDGET_CLOSEDpattern: not a reservation-lifecycle denial, so no exception class orbuildProtocolExceptionmapping.Retry-Afterheader exposure: the client now captures the HTTPRetry-Afterheader (how 429 rate-limit responses carry the delay per the spec) and exposes it asCyclesResponse.retryAfterMsHeader(seconds → ms; non-integer forms ignored gracefully).buildProtocolExceptionfalls back to it forretryAfterMswhen the body carries noretry_after_msfield (body wins when both are present). No auto-retry behavior change — the delay is surfaced, not consumed.- Regression coverage confirms
listReservationsforwards and URL-encodes the additivefrom/to,expires_from/expires_to, andfinalized_from/finalized_toISO-8601 query parameters. The existingparams?: Record<string, string>API already accepted them.
- npm publish now uses npm Trusted Publishing (OIDC) instead of the long-lived
NPM_TOKENsecret (NODE_AUTH_TOKENremoved from the publish job; the job already hadid-token: writeand upgrades npm, which OIDC requires at >= 11.5.1). The trusted publisher must be configured for theruncyclespackage on npmjs.com before the next tagged release. Mirrors the same change incycles-mcp-server, whose v0.3.0 release initially failed on an expired token. package.jsonrepository.urlnormalized togit+https://...pernpm pkg fix, which also makes it match the exact form npm's trusted-publisher repository check expects.- Refreshed the vendored
cycles-protocol-v0.yamlcontract fixture from v0.1.24 to the current v0.1.25.15 and aligned the exactErrorCodecontract assertion withLIMIT_EXCEEDEDandTENANT_CLOSED.
- Forced transitive
esbuildto >= 0.28.1 via npmoverrides, resolving Dependabot alert #9 (low severity, dev-only: arbitrary file read via the esbuild development server on Windows;tsuppinsesbuild ^0.27.0so no direct range reaches the patched version). Remove the override oncetsupallows esbuild >= 0.28. - Updated test-only transitive
fast-urifrom 3.1.2 to 3.1.4, resolving the high-severity host-confusion advisories reported through the Ajv contract-test toolchain. The dependency is not included in the published package.
eventCreateResponseFromWirenow maps the declaredEventCreateResponse.chargedfield. Previously, the effective charge onALLOW_IF_AVAILABLE-capped events was silently lost and always appeared asundefined.- README error-handling docs no longer describe
CyclesTransportErroras thrown on network failure — the SDK never constructs it. Reservation-time transport failures surface asCyclesProtocolErrorwithstatus: -1(withCycles/reserveForStream) or asCyclesResponsewithisTransportError/status: -1(programmatic client); commit-time failures are retried in the background bywithCycles, whileStreamReservation.commit()throws and resetsfinalizedfor caller retry or release. The class remains exported for use in user code; a new "Transport failures (status -1)" README subsection documents the actual behavior. examples/vercel-ai-sdkchat route no longer mixes AI SDK v4 and v5 APIs (it compiled under neither whilepackage.jsonpins"ai": "^4.0.0"): now pure v4 —Messagetype andconvertToCoreMessagesreplace v5'sUIMessage/convertToModelMessages.runcyclesusage unchanged.
- Library changes are additive or bug fixes; there is no breaking API or wire-format change.
- Evidence/JWKS endpoints and the remaining additive response-mapping work are outside this release and remain tracked in #134.
- 339 tests pass; coverage is 98.61% statements and 99.81% lines. Lint, typecheck, build, dependency audit, and package dry-run are clean.
npm metadata refresh for category-search discovery. No code changes — bundle and runtime behavior are identical to 0.3.0.
package.json: rewrotedescriptionto lead with the cost / action / audit pillars ("TypeScript AI agent runtime control — enforce LLM cost limits, action permissions, and audit trails for agents before execution.") and expandedkeywordsfrom 15 to 26. Drops legacy keywords (billing,metering,api-client,ai,llm,agents,token-budget,spend-limit) in favor of category-search variants (ai-agent,agent-budget,budget-control,cost-enforcement,spending-limit,llm-cost,runtime-authority,action-control,action-authority,audit-trail,audit,compliance,multi-tenant) plus framework targeting (langchain,langgraph,openai-agents,vercel-ai-sdk,mcp).
Java parity: dynamic subject and action fields on withCycles.
- Dynamic subject + action fields on
withCyclesconfig —tenant,workspace,app,workflow,agent,toolset,actionKind, andactionNamenow accept(...args: TArgs) => string | undefinedin addition to a static string. Callables are resolved against the wrapped function's per-call args; returningundefinedfalls through to the client-config default (subject) or"unknown"(action). Static strings unchanged. Java parity withcycles-spring-boot-starter#50. (#72)
Bug fixes, support 0.1.24 spec.
- Add badges to README for npm, CI, and license (#24)
- Add documentation links section to README (#25)
- Add budget and extension error codes, charged amount to event response (#29)
- Document nested withCycles behavior and recommended patterns (#26)
- Claude/analyze spring issue 29 v biy9 (#27)
- Change default overage policy from REJECT to ALLOW_IF_AVAILABLE (#28)
- chore: bump version to 0.2.0 for protocol v0.1.24 (#30)
Fix type safety in WithCyclesConfig generics.
- Add AUDIT.md documenting protocol conformance (#19)
- Add AWS Bedrock and Google Gemini budget governance examples (#20)
- Add parent README for examples directory (#21)
- Add API key creation guide to documentation and examples (#22)
- Fix type safety in WithCyclesConfig generics and add compile-time type tests (#23)
Updates and bug and stability fixes, more SDK examples.
- Add manual workflow_dispatch trigger to CI publish (#4)
- Add comprehensive test coverage for lifecycle, streaming, and error handling (#7)
- Add comprehensive examples for Cycles budget governance (#9)
- Claude/expand ai examples zj dwy (#10)
- Add ESLint with typescript-eslint/recommended and coverage thresholds (#12)
- Add lint and coverage enforcement to CI (#13)
- Add test for commit retry exhaustion warning (#18)
- Comprehensive README rewrite for npm publication (#5)
- Optimize initialization and add async disposal support (#6)
- Update TEST_COVERAGE_ANALYSIS.md with final coverage results (#8)
- Document withCycles client caching behavior in default client section (#11)
- Document commit rollback behavior for failed commits in streaming sec… (#14)
- Warn on commit retry exhaustion in CommitRetryEngine (#15)
- Remove dead code: unused constants, validateReservationId, makeClient (#16)
- Remove CyclesTransportError from public exports (#17)
Initial release.