Skip to content

feat(plugins): add extensible client architecture - #1061

Merged
jlucaso1 merged 50 commits into
mainfrom
agent/plugin-architecture
Jul 22, 2026
Merged

feat(plugins): add extensible client architecture#1061
jlucaso1 merged 50 commits into
mainfrom
agent/plugin-architecture

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Status

Ready for review; Phase 3 and the selected pre-merge extensibility foundations are complete.

This remains a single, cumulative PR for the plugin-architecture refactor. Phase 4 items are demand-driven and outside this delivery. Current head 30d06ff6 includes origin/main at aa2582ca (including #1070 and #1069). The complete local suite, all-test Clippy, minimal native/WASM plugin builds, and all 40 plugin-host tests are green on current head 30d06ff6; the final A/B remains green at 60fda0bc. All review threads are resolved, and GitHub checks plus fresh AI reviews are rerunning on the published head. No merge into main was performed.

  • Phase 0 — removable event subscriptions, aggregate interest fast path, raw-node forwarding leases
  • Phase 1 — canonical client construction and generation-scoped extension lifecycle
  • Phase 2 — native plugin model and event routing
    • Phase 2.1 — transactional native Rust host, typed APIs, capabilities, dependency ordering, and scoped tasks
    • Phase 2.2 — bounded PluginEventRouter keyed by (plugin_id, topic)
    • Phase 2.3 — runtime-agnostic task clock and external native metrics vertical slice
  • Phase 3 — hardening, observability, conformance, and stabilization
    • Phase 3.1 — scoped task completion barriers before connection and terminal teardown
    • Phase 3.2 — lifecycle queue/reconnect hardening and activation gates
      • bounded Ready admission with lossless per-generation Closed delivery
      • synchronous scope retirement when reconnect is requested
      • shutdown-safe plugin activation and client startup gate
      • lifecycle integration fully absent unless client-lifecycle or plugins is enabled
    • Phase 3.3 — native plugin observability, conformance, and stabilization
      • on-demand host/plugin health and lifecycle/task/resource snapshots
      • per-publisher/router delivery, backpressure, and unique queue-memory accounting
      • public-only metrics-plugin conformance coverage and strict native/WASM validation
      • persisted host invariants and future-adapter seam in agent_docs/plugin_architecture.md
      • manifest-ID-keyed untyped adapters, owned core-event subscriptions, and cooperative task draining
      • bounded installation with partial LIFO rollback and configurable host deadlines
      • isolated A/B benchmark and stabilization sign-off

The bridge and baileyrs are not implementation dependencies of this PR. The native seams are being designed so a foreign-language adapter can be added later without rebuilding the client lifecycle or plugin model. The foreign command/protocol boundary remains deliberately deferred until a real bridge consumer is in scope.

Why

Client construction was split between high-level and low-level paths, and background services could only be coordinated through client-specific initialization. There was also no public lifecycle model aligned with the client's existing connection generation, which a plugin host needs to cancel connection-owned work reliably across expected disconnects, failures, reconnects, and terminal shutdown.

The existing event bus also had permanent registrations, evaluated handler interest dynamically under a read lock, and exposed raw-node forwarding as a global last-write-wins boolean. Those contracts could not safely support independently owned plugin or foreign-consumer subscriptions.

Phases 0 and 1 establish those foundations. Phase 2 now adds the native host and bounded custom-event routing without introducing the foreign protocol or bridge implementation yet, keeping that later boundary driven by a real foreign vertical slice.

What changed in Phase 0

  • replace permanent event registration with Subscription, a #[must_use] RAII token that unregisters on drop
  • capture explicit EventInterest at subscription time and support controlled updates through Subscription::update_interest
  • maintain aggregate interest in two portable AtomicU64 words, making has_handler_for lock-free without requiring unavailable AtomicU128
  • preserve copy-on-write dispatch snapshots: an in-flight dispatch may finish once after unsubscribe, while later dispatches cannot see the removed handler
  • replace raw-node forwarding's shared boolean with reference-counted RawNodeLease ownership, so independent consumers cannot disable one another
  • correct the documented EventInterest capacity from 64 to 128 kinds
  • migrate all internal registrations to the ownership-aware API

What changed in Phase 1

  • add a canonical, runtime-validated ClientBuilder and typed ClientBuilderError
  • split client construction into inert assembly and explicit service startup so no task observes a partially installed client
  • route BotBuilder through the canonical pipeline while preserving its typestate API
  • centralize cache, runtime, transport, HTTP, persistence, encryption-handler, durability, history, pre-key, resend, instrumentation, and background-saver configuration
  • add an aggregate ClientLifecycle seam installed through Weak<Client>, with rollback before publication on installation failure
  • formalize ConnectionScope around the existing connection generation: ready after authentication, synchronous cancellation immediately after generation bump, and close only after authoritative cleanup
  • serialize lifecycle callbacks through one isolated driver with timeout and panic isolation, bound stale Ready work at 64 entries, and preserve every per-generation Closed callback before terminal Shutdown
  • make final Closed publication atomic with terminal Shutdown, preserving lifecycle order under races
  • move lifecycle-enabled cleanup into an authoritative detached task so cancelling its waiter cannot strand a generation; lifecycle-free clients retain the direct fast path
  • add terminal, idempotent and cancellation-safe async shutdown that waits for an active connection scope to close
  • preserve explicit logout ordering and cancel-only behavior from synchronous drop/shutdown signaling

What changed in Phase 2.1

  • add ClientPlugin { type Api; manifest(); install(); on_ready(); on_closed(); shutdown() } for trusted native plugins registered at build time
  • support registration from both ClientBuilder and the typestate-preserving BotBuilder
  • expose type-safe APIs through client.plugin::<P>() -> Option<Arc<P::Api>>, keyed by the plugin marker rather than the API type, so two plugins may expose the same Rust type without collision
  • validate manifests before client assembly: IDs, versions, duplicate marker/manifest IDs, duplicate or missing dependencies, and dependency cycles
  • resolve dependencies topologically and expose only direct, declared dependency APIs during installation
  • install transactionally while the client is inert; publish the API typemap only after every plugin succeeds, and roll back the failing plugin, prior plugins, and the upstream lifecycle in LIFO order
  • compose an existing ClientLifecycle outside the plugin order instead of replacing it
  • shape native access through small capability handles for core events, tasks, messaging, and typed IQ; no backend or Signal store handle is exposed, and native capabilities are explicitly not a sandbox
  • keep capability handles on Weak<Client> plus plugin resource state, avoiding Client -> plugin API -> Client cycles and rejecting use before publication or after shutdown
  • distinguish install-scoped tasks from generation-scoped tasks; install work starts only after the complete plugin set and client services are published, survives reconnects, and is cancelled on rollback/terminal shutdown, while connection work is cancelled with its ConnectionScope
  • retain core-event subscription leases as plugin resources; synchronously cancel tasks/subscriptions, then await tracked install- and connection-scoped task completion before on_closed, rollback, or LIFO shutdown hooks
  • isolate manifest/install/lifecycle panics, including panics while polling returned futures
  • keep native type erasure compatible with native Send + Sync and the existing WASM MaybeSendSync convention, without unsafe code
  • gate the entire native host behind the opt-in Cargo feature plugins; default/no-plugin builds have no plugin field, branch, allocation, or linked code

What changed in Phase 2.2

  • add a PluginEventRouter separate from CoreEventBus, so custom plugin traffic neither changes the sealed core Event contract nor consumes EventInterest bits
  • allocate the router only when an installed manifest requests PluginCapability::PluginEvents
  • bind publication authority to the installing plugin ID through PluginEvents; a plugin cannot publish into another plugin namespace
  • route exact (plugin_id, topic) selectors before a native endpoint or future FFI adapter is awakened
  • give every endpoint an independently bounded queue with explicit DropNewest / DropOldest behavior and cumulative delivery/drop/depth statistics
  • cap one endpoint at 1,024 selectors and 65,536 queued envelopes, rejecting invalid publishers, topics, capacities, and schema version zero with typed errors
  • share immutable Arc<PluginEventEnvelope> payloads across matching endpoints and carry schema version, encoding, connection generation, and route-local sequence
  • serialize sequence assignment with fan-out for concurrent publisher order; discarded events consume a sequence number so consumers can detect loss, and a route resets only after its last subscriber leaves
  • keep publication non-blocking with copy-on-write route endpoint slices; shutdown rejects new work while allowing receivers to drain already queued envelopes
  • cover exact routing, both overflow policies, concurrent order, generation capture, closure/drain semantics, and a typed native plugin vertical slice

What changed in Phase 2.3

  • add the target-correct PluginFuture<'a, T> alias so public plugin futures are Send on native and remain valid on single-threaded WASM
  • add cancellation-aware PluginTasks::sleep and PluginConnectionTasks::sleep; install-scoped clocks survive reconnects while connection-scoped clocks terminate with their generation
  • add plugins/metrics as an external, unpublished workspace crate that consumes only the public plugin API, proving the host is usable outside the main crate
  • expose a type-safe MetricsApi through client.plugin::<MetricsPlugin>() with sealed snapshot payloads and a reusable typed tick selector
  • count only selectively subscribed core events, publish periodic JSON ticks only when the custom-event route has subscribers, and account for enqueue/drop/serialization failures
  • keep the periodic worker install-scoped, keep connection generation state connection-scoped, and verify stale scopes cannot clear a newer generation
  • exercise bounded backpressure, typed API lookup, reconnect-surviving install work, generation cancellation, and shutdown through public-only integration tests
  • leave bridge adapters, wire schemas, grants, and foreign commands out of this slice; the native vertical slice supplies the concrete semantics those future boundaries must preserve

Review hardening through Phase 3.2

  • make staged plugin installation cancellation-safe: dropping ClientBuilder::build() closes resources immediately and schedules bounded LIFO shutdown for the current plugin, previously installed plugins, and the installed upstream lifecycle
  • bound every plugin/upstream lifecycle callback independently so one stalled plugin cannot suppress later callbacks; upstream readiness failures are aggregated instead of short-circuiting plugin readiness
  • retain a RawNodeLease whenever a plugin subscribes to EventKind::RawNode
  • replace the unsafe standalone ClientBuild::client() path with consuming into_client(), which starts the default major-sync worker; advanced hosts still use into_parts() and own the sole receiver
  • isolate both synchronous and polled panics from ClientLifecycle::install as typed ClientBuilderError::LifecycleInstall failures
  • add the non-blocking ClientLifecycle::signal_shutdown boundary so FFI-style synchronous shutdown closes plugin tasks, subscriptions, leases, and capability handles even while other references remain alive
  • reject Duration::ZERO for the background saver instead of starting a permanent hot loop
  • request terminal lifecycle shutdown before the first cancellable disconnect() await, closing plugin resources immediately while preserving the durability flush and connection-shutdown ordering
  • detach explicit install rollback before awaiting it, so aborting a failed build during a stalled hook cannot suppress the remaining LIFO callbacks
  • synchronously signal an installed upstream lifecycle before any asynchronous rollback callback; panic isolation and idempotence remain enforced
  • release the event-bus write lock before dropping retired copy-on-write snapshots, so a handler destructor may unsubscribe another handler from the same bus without deadlocking
  • serialize authentication-success scope publication against connection-generation teardown, so a rejected or obsolete scope cannot leave the client reporting a logged-in state
  • take plugin-owned subscriptions out of the resource mutex before destroying them, allowing handler teardown to reenter subscription APIs without deadlock
  • destroy subscriptions rejected after shutdown only after releasing the resource mutex, covering the symmetric reentrant-handler path
  • serialize Connected publication against generation and terminal cancellation, while allowing a Connected handler to signal shutdown reentrantly without deadlock
  • make PluginContext dependency views weak and non-owning, so an API may retain its context without creating an ApiRegistry -> API -> context -> registry cycle
  • signal standalone lifecycle resources when the last Arc<Client> drops, and make the device-registry waiter own only a shutdown signal so it cannot keep the client alive forever
  • propagate panics from detached authoritative cleanup back to its waiter through a cancellation-safe oneshot, preventing a custom transport panic from hanging disconnect/run forever
  • gate Client::run() on successful construction/lifecycle activation, so a leaked client cannot connect while plugin installation is incomplete or after construction was rejected
  • make lifecycle/plugin activation sticky and terminal: shutdown racing installation rejects the build, closes staged resources, and cannot later republish the host
  • retain staged plugin APIs through detached rollback, so API destructors cannot run before their plugin/upstream shutdown barriers and hooks
  • keep standalone lifecycle integration behind client-lifecycle; plugins enables it transitively, while default clients retain the lifecycle-free field/layout and cleanup fast path
  • close a retired ConnectionScope from an unwind guard immediately after the generation bump, so cancellation or a transport panic cannot strand terminal shutdown
  • make detached installation rollback completion unwind-safe even when a staged API destructor panics; later waiters always observe completion
  • race standalone and plugin installation against terminal shutdown, close staged plugin resources synchronously, and cancel non-cooperative install futures without requiring them to wake themselves
  • isolate each plugin-owned subscription destructor and each plugin resource closure during shutdown/rollback, so one panicking handler cannot strand later plugins or the upstream lifecycle
  • retire generation task trackers from the sticky ConnectionScope cancellation signal, so task cleanup does not depend on lifecycle callback scheduling
  • explicitly poll and destroy lifecycle/plugin futures inside unwind barriers, including timeout cancellation and normal installation teardown, so a panicking future destructor cannot strand later callbacks
  • preserve every queued scope closure under callback pressure; stale Ready work remains bounded, while lossless Closed delivery may temporarily exceed the target before ordered Shutdown

What changed in Phase 3.3

  • add Client::plugin_stats() with per-manifest lifecycle state, sticky health, callback/task failures, active task generations, subscriptions, and isolated teardown/core-event panics
  • add per-publisher PluginEvents::stats() plus aggregate PluginEventRouter::stats() for delivery, backpressure, endpoint capacity, and queue occupancy
  • share one internal queued envelope across fanout and count its payload memory once until the final endpoint releases it
  • integrate plugin counts and unique queued custom-event bytes into the existing on-demand MemoryReport, fully gated behind plugins
  • isolate plugin core-event handler execution and delayed handler destruction so one plugin cannot unwind through the core dispatcher or suppress unrelated teardown
  • derive health per plugin from cumulative lifecycle errors/panics, timeouts, task-drain timeouts, event publication failures/drops, and isolated resource failures; unaffected plugins remain healthy
  • extend the external metrics plugin test through public APIs to prove backpressure is visible as degraded health without bridge-specific code
  • keep snapshots approximate and allocation-on-demand; default/no-plugin builds contain none of the observability fields, counters, or branches
  • document construction, type-safe APIs, capabilities, lifecycle/task ownership, event/backpressure rules, diagnostics, and the deliberately deferred foreign-adapter contract in agent_docs/plugin_architecture.md

Latest review hardening

  • bind readiness and Connected publication to the authentication task generation, serialized against authoritative cleanup; a stale success task cannot claim a replacement scope
  • isolate install- and connection-scoped plugin-task panics, attribute them to the owning plugin_id, increment PluginStats::task_panics, and make plugin health sticky-degraded
  • clarify task ownership: cancellation is signalled synchronously, while destruction is cooperative and awaited only within the documented bounded drain
  • publish feature-gated API badges on docs.rs and document feature discovery for direct consumers and external plugin crates
  • gate direct Client::connect() on successful construction activation, matching run() and preventing leaked install-time handles from starting transport early
  • attribute DropOldest and DropNewest losses to the publisher that owned the discarded envelope, including cross-namespace endpoint queues; the per-call report still records the eviction caused by that call
  • keep plugin APIs, manifests, diagnostics, and custom-event routing staged until final lifecycle activation commits the build; a rejected construction never exposes plugin-owned state
  • serialize plugin publication and lifecycle activation with terminal shutdown; APIs and every resource become active before install-scoped tasks are awakened, with no fallible publication step after work can run
  • compile the login/publication synchronization mutex and all of its lock paths only with client-lifecycle, preserving the default client layout and fast path
  • preserve every mandatory Closed callback under pressure while coalescing replaceable readiness work to the latest Ready, even when closures already exceed the soft queue target
  • expose sticky upstream lifecycle failure/timeout counters and include them in aggregate plugin-host health without degrading unaffected plugins
  • add public manifest-ID-keyed UntypedClientPlugin registration, including Arc<dyn UntypedClientPlugin>, so a future bridge can host multiple runtime-defined instances without native TypeId collisions
  • make plugin core-event registrations explicitly owned: the returned token supports early unsubscribe and atomic interest/RawNodeLease updates, and host shutdown invalidates retained tokens
  • store retained subscription registrations weakly and self-prune them during close, so drop/unsubscribe churn releases both the handler allocation and registry slot immediately
  • distinguish abort-on-cancel tasks from cooperative tasks that observe install or connection cancellation, with independently configurable callback and task-drain deadlines
  • bound plugin and upstream installation separately (30 seconds by default), cancel yielding installs on timeout, and run partial cleanup through the same LIFO rollback contract
  • widen the private capability set to 64 bits and verify current identifiers remain distinct, leaving room for storage and middleware capabilities without another representation change

Cargo feature policy

plugins remains opt-in, and client-lifecycle remains the smaller opt-in seam for hosts that do not need the plugin host. External plugin crates should enable whatsapp-rust/plugins on their dependency; Cargo feature unification then activates it transitively, so application users do not need to discover a second switch. docs.rs builds with plugins and labels gated APIs.

LTO is an optimization, not an API or size guarantee. In the current release harness, enabling plugins without installing a plugin had equivalent throughput but increased the stripped binary by 180,104 bytes (+0.89%). Keeping the host opt-in therefore avoids a permanent size tax while preserving an uncomplicated consumer path. No additional fine-grained plugin feature matrix is planned.

Known limitations and pre-merge decisions

This checklist is intentionally honest about the current boundary. Checked items are foundations committed in this PR; unchecked items remain explicit limitations, not behavior implied by the API.

Foundations added before merge

  • Public manifest-ID-keyed untyped plugin registration for future foreign adapters; TypeId remains only the native typed API lookup key.
  • Plugin-owned core-event subscription handles with early unsubscribe, explicit interest updates, and immediate self-pruning registry cleanup.
  • Abort-on-cancel and cooperative tracked-task modes with host-configurable install/callback/task-drain deadlines.
  • Bounded plugin/upstream installation and partial LIFO rollback, including cancellation-time destructor isolation.
  • A 64-bit capability set with collision coverage, leaving expansion room for future narrow handles.

Deliberately deferred until a real vertical slice

  • Bounded foreign core-event endpoints with schema generation and drift tests.
  • Versioned foreign command/event protocol with scoped runtime grants.
  • Namespaced plugin storage with quota, migration, transaction, and cleanup semantics.
  • Per-plugin resource budgets for task count, queued bytes, and payload size, plus task naming.
  • Queued/watchdog delivery for core-event handlers; native handlers remain inline and must offload slow work.
  • Preemptive isolation for code that blocks an executor thread; native deadlines can cancel only futures that yield.
  • Dependency version ranges, optional dependencies, conflicts, and declared event schemas.
  • Dynamic install/uninstall or marketplace loading.
  • Ingress/send middleware and privileged pre-ack interception.
  • Sidecar, WASM Component, or sandbox execution.

Native plugins are trusted in-process code. Capabilities shape the API they receive; they are not a security boundary. Runtime enforcement belongs at a future foreign-process/WASM boundary. The unchecked deferred items will not be silently approximated in this PR.

Consumer migration guide (breaking changes)

This guide compares the current main API with this PR's current head. It will be updated whenever a later commit changes a public contract. Phase 2.2/2.3 plugin additions are opt-in and do not add source-migration requirements for existing consumers; 06050d0c adds the drop-time lifecycle guarantee and 5b4a3b4f makes scoped task teardown an awaited barrier before plugin hooks. 47dcf10a, 1ee952b9, and c86f6d52 harden lifecycle ordering/construction without widening the public API. 63bb4d95 makes standalone lifecycle APIs explicitly opt-in through client-lifecycle; the plugins feature enables it transitively. 54cbf95c hardens unwind and terminal-install behavior without widening the public API. 271d8423 isolates destructor panics during resource teardown, e2f6bc4c makes scope retirement and future cancellation unwind-safe, and e72f2729 makes per-generation closure delivery lossless under queue pressure. 667689ab adds opt-in observability snapshots and counters, 3244e32c only clarifies an internal lock contract, and dc55c1e3 persists the architecture contract. None adds a source-migration requirement. The latest hardening adds the non-exhaustive PluginStats::task_panics diagnostic and clarifies cooperative task cancellation; ordinary consumers require no change, while plugin authors should make long-running work observe the provided shutdown signal or use the cancellation-aware task helpers. 7f025d32 gates direct connect during construction, and b4b9c63c corrects cross-namespace overflow attribution. These are behavioral fixes and add no source-migration requirement; telemetry consumers should read PluginEventPublishReport::dropped as loss caused by that publish call and PluginEventPublisherStats::dropped as loss of envelopes owned by that publisher. f1a8138b delays every public plugin surface until the final construction commit; normal post-build consumers require no change. 29481d9e adds manifest-keyed untyped registration, 03c4b8bb makes plugin core-event subscriptions owner-scoped, 1444153b adds cooperative task draining, 60fda0bc bounds installation/partial rollback, bfdb2699 makes closed subscription registry cleanup immediate, 21b430c2 records that ownership contract in the RFC, and 30d06ff6 widens the private capability mask for future handles. These are additive relative to main; plugin authors adopting this PR must retain subscription tokens and choose task cancellation semantics explicitly.

Upgrade checklist

Consumer pattern Required action
BotBuilder::with_event_handler(...) and closure-based bot handlers None; these APIs keep their existing ownership behavior.
Client::register_handler(...) or CoreEventBus::add_handler(...) Migrate to a retained Subscription, or explicitly call .detach() for process-lifetime registration.
A mutable EventHandler::interest() implementation Move changes to Subscription::update_interest(...); interest is captured once at registration.
Client::set_raw_node_forwarding(bool) Hold a RawNodeLease from acquire_raw_node_forwarding() for exactly as long as forwarding is needed.
Exhaustive matching on BotBuilderError::UnsupportedDurabilityBackend Match the nested BotBuilderError::Client(ClientBuilderError::UnsupportedDurabilityBackend(_)).
Client::new(...) / Client::new_with_cache_config(...) None; both compatibility constructors remain available. The new low-level builder is optional.
Default-feature consumers not using plugins or lifecycle extensions None; both subsystems and their runtime branches are absent from default builds.
Consumers using ClientLifecycle, ConnectionScope, or ClientBuilder::with_lifecycle without plugins Enable Cargo feature client-lifecycle. Consumers enabling plugins already receive it transitively.
Custom plugin-event consumers Retain PluginEventSubscription, choose a bounded queue/overflow policy, and route by (plugin_id, topic, schema_version); EventInterest::ALL does not include custom events. Treat publish-report drops as call-attributed and publisher-stat drops as discarded-envelope-owner-attributed.
Plugin code calling PluginCoreEvents::subscribe(...) Retain the returned PluginCoreEventSubscription in plugin-owned state/API. Dropping it now unregisters immediately; use update_interest or unsubscribe for explicit changes.
Runtime-defined or future foreign adapters Implement UntypedClientPlugin and register by manifest ID with with_untyped_plugin(...) / with_untyped_plugin_arc(...); this path intentionally exposes no Rust Client::plugin::<P>() API.
Long-running plugin tasks Use spawn for abort-on-cancel work. Use spawn_cooperative only when the future observes shutdown_signal() / cancellation_signal() and can finish within the configured drain deadline.
Hosts with slow plugin initialization or cleanup Configure PluginHostConfig; install defaults to 30 seconds, callbacks/drains to 5 seconds, and zero durations are rejected. A timed-out install is rolled back.
Code using plugin/lifecycle resources during terminal Client::disconnect() Stop that work before calling disconnect; terminal resource invalidation is now synchronous at the start of the call.
Dropping the final Arc<Client> without disconnect().await No source change. Client::Drop now emits the synchronous lifecycle signal, but durable flush/transport cleanup still require explicit async disconnect.

1. Retain event subscriptions

Registrations now have an owner. Dropping the returned token unregisters the handler, so ignoring the result no longer creates a permanent registration.

Before:

client.register_handler(handler);
// or, for direct wacore users: core_event_bus.add_handler(handler);

After, with scoped ownership:

let subscription = client.subscribe_handler(handler);

// Keep `subscription` in the component that owns the handler.
// Dropping it unregisters the handler.
drop(subscription);

For an explicit filter:

let subscription = client.subscribe(
    EventInterest::of(&[EventKind::Messages, EventKind::Receipt]),
    handler,
);

If the old process-lifetime behavior is genuinely intended, make that choice explicit:

client.subscribe_handler(handler).detach();

An in-flight dispatch may finish once from its previous immutable snapshot after unsubscribe; subsequent dispatches cannot see the removed handler.

2. Update dynamic event interest through the token

EventHandler::interest() is now a registration-time hint. The bus does not call it for every event. Code that previously mutated handler state and expected interest() to widen automatically must retain its Subscription and update the filter explicitly:

let subscription = client.subscribe_handler(handler);
subscription.update_interest(EventInterest::of(&[
    EventKind::Messages,
    EventKind::Connected,
]));

This contract enables the lock-free aggregate-interest fast path while keeping changes explicit and race-safe.

3. Replace the raw-node boolean with an ownership lease

Before:

client.set_raw_node_forwarding(true);
// consume raw nodes
client.set_raw_node_forwarding(false);

After:

let raw_forwarding = client.acquire_raw_node_forwarding();
let raw_events = client.subscribe(
    EventInterest::of(&[EventKind::RawNode]),
    handler,
);

// consume raw nodes

drop(raw_events);
drop(raw_forwarding);

Each consumer owns an independent lease. One consumer can no longer disable forwarding while another still needs it. Plugin core-event subscriptions acquire the corresponding raw-node lease automatically.

4. Adjust BotBuilderError pattern matches

Durability validation moved into the canonical client builder, so the old top-level variant is now nested.

Before:

match Bot::builder() /* ... */ .build().await {
    Err(BotBuilderError::UnsupportedDurabilityBackend(reason)) => {
        // handle unsupported durability backend
    }
    result => { /* ... */ }
}

After:

match Bot::builder() /* ... */ .build().await {
    Err(BotBuilderError::Client(
        ClientBuilderError::UnsupportedDurabilityBackend(reason),
    )) => {
        // handle unsupported durability backend
    }
    result => { /* ... */ }
}

Both error enums are #[non_exhaustive]; downstream matches should retain a fallback arm.

5. Optional low-level builder migration

The existing Client::new* constructors remain source-compatible. Dynamic hosts and FFI layers may instead use the new runtime-validated builder:

let client = Client::builder()
    .with_runtime(runtime)
    .with_persistence_manager(persistence)
    .with_transport_factory(transport)
    .with_http_client(http)
    .build()
    .await?
    .into_client();

Use into_client() for a standalone client; it starts the standard major-sync worker. Only advanced hosts that take responsibility for draining the sole receiver should call into_parts().

6. Account for terminal lifecycle timing

Client::disconnect() now invokes the prompt, idempotent ClientLifecycle::signal_shutdown boundary before its first cancellable await. Plugin tasks, subscriptions, raw-node leases, and capability handles therefore stop immediately when terminal disconnect begins; the durability flush, outbound flush, and connection-shutdown notification retain their existing order afterward. Consumers must not depend on plugin capabilities remaining usable during that terminal flush window.

Dropping the final Arc<Client> invokes the same synchronous signal so standalone lifecycle work cannot remain parked. This is not a substitute for disconnect().await: Drop cannot perform the async durability flush or transport teardown.

7. Enable standalone lifecycle APIs explicitly

Lifecycle extension APIs no longer contribute fields, branches, callback-driver code, or binary-size cost to the default client. Consumers that use lifecycle integration directly must opt in:

whatsapp-rust = { version = "...", features = ["client-lifecycle"] }

The plugins feature includes client-lifecycle, so native plugin consumers do not need to list both. Default consumers that use neither API require no source or manifest change.

New plugin contract (opt-in, not a migration requirement)

Enable Cargo feature plugins, register native plugins only during build, and retrieve their typed API by marker:

let client = Client::builder()
    // required platform dependencies...
    .with_plugin(MetricsPlugin::new())
    .build()
    .await?
    .into_client();

let metrics: Arc<MetricsApi> = client
    .plugin::<MetricsPlugin>()
    .expect("MetricsPlugin was registered");

Plugin API handles survive reconnects; connection-scoped work does not. Native plugins are trusted in-process Rust code, and capability handles shape available APIs without claiming sandbox enforcement.

Dependency lookup through PluginContext::plugin::<P>() is deliberately non-owning at the context level. If an API retains its PluginContext and needs a dependency later, it must retain the returned Arc<P::Api> during installation; retaining the context alone does not keep dependencies or the staging registry alive.

The reference metrics plugin demonstrates the complete type-safe path:

let metrics = client
    .plugin::<MetricsPlugin>()
    .expect("metrics plugin installed");
let snapshot: MetricsSnapshot = metrics.snapshot();
let selector: PluginEventSelector = metrics.tick_selector();

Subscribe to custom plugin events

Custom events use their own exact, bounded router. They are intentionally not delivered through CoreEventBus, and EventInterest::ALL continues to mean all core events only.

let router = client
    .plugin_event_router()
    .expect("at least one installed plugin publishes custom events");
let tick = PluginEventTopic::new("tick")?;
let events = router.subscribe(
    [PluginEventSelector::new("metrics", tick)?],
    PluginEventEndpointConfig::new(256, PluginEventOverflow::DropOldest),
)?;

while let Ok(event) = events.recv().await {
    // Dispatch by event.plugin_id/topic/schema_version and decode event.payload.
}

Retain PluginEventSubscription for as long as the endpoint should exist. Select a queue capacity and overflow policy deliberately, inspect events.stats() for loss/backpressure, and treat sequence gaps as discarded events. This is a new opt-in API; existing core-event consumers require no migration.

Main synchronization

Merge commit d4abef10 brings in current origin/main at aa2582ca. That mainline includes #1070's removal of the obsolete device-registry cleanup task and #1069's typed stanza-response/retry work. The synchronization was conflict-free. The plugin host continues to expose only high-level messaging/IQ capabilities, and no new plugin branch was added to stanza or retry hot paths.

Current head 30d06ff6 is based on that main commit. The definitive non-E2E workspace suite, all-test Clippy, formatting, minimal plugin build, and plugin-enabled WASM build pass locally. The local E2E binary still requires the repository's Bartender service; the GitHub E2E job is authoritative and is rerunning on this push. No benchmark or project manifest was edited for this synchronization or the final A/B.

Performance

Current whole-branch A/B after main synchronization

The definitive comparison uses clean main aa2582ca and head 60fda0bc. Both release clients were prebuilt from isolated source paths, passed through CLIENT_BIN_OVERRIDE, and exercised on ports 18082/18083 without editing any benchmark Cargo.toml. Plugins were disabled, so this measures the ordinary consumer hot path. Five clean runs per side sent 30,000 ping-pongs at a 3,000/s cap; one additional PR run was discarded in full because host CPU pressure reached 5.17% versus roughly 0.16-0.22% in the retained batch.

Main → PR medians were 2,859.33/s → 2,859.23/s throughput, 3.770 s → 3.580 s client CPU, 36.4 MB → 35.4 MB HWM, 0.54 ms → 0.56 ms ack latency, and 0.58 ms → 0.59 ms pong latency. The throughput ranges overlap (2,848.38-2,859.62/s versus 2,847.89-2,859.51/s), and all retained runs delivered 150,000 acknowledgements and pongs per side with zero loss or failures. The non-LTO release harness artifact changed from 27,094,008 to 26,807,696 bytes (-286,312 / -1.06%). No measurable feature-disabled regression is present.

Phase 3.3 observability

The isolated A/B compares e72f2729 (before native observability) with 667689ab (observability). Both release clients enabled whatsapp-rust/plugins but installed no plugin, so this measures the unused opt-in runtime path. Cargo paths overrides, dedicated target/lock paths, and distinct ports kept the source and runs isolated; the pre-existing benchmark Cargo.toml and Cargo.lock retained SHA-256 aea5f2e39dd88d569fcca4db37728c10ce04ea283033ee6858a77791ccaf2d1d and d81fa43e9802b569845eba1f24b219430d378e108cb87cf07cca1ae4ae52b19b throughout.

Ten order-alternated normal pairs ran on ports 18621–18640, with 30,000 ping-pongs at 3,000/s per run. Baseline → candidate side medians were 2,859.175/s → 2,859.280/s throughput, 3.460 s → 3.570 s client CPU, 26.65 → 26.75 MB RSS average, 35.45 → 35.45 MB HWM, and 9.95 → 9.95 MB anonymous RSS average. Four order-alternated stress pairs ran on ports 18641–18648, with 100,000 ping-pongs at 6,000/s per run: 5,846.965/s → 5,847.030/s, 9.000 s → 9.105 s client CPU, 39.85 → 39.80 MB RSS average, 55.20 → 55.05 MB HWM, and 22.95 → 23.00 MB anonymous RSS average. All 28 runs completed; each side delivered 700,000 acknowledgements and pongs with zero loss or pong failures.

The CPU samples do not establish a stable regression: normal paired deltas ranged from -0.12 s to +0.44 s with a +0.035 s median, while stress ranged from -0.39 s to +0.49 s with a +0.105 s median; median host interference also moved in opposite directions between the batches (4.0% → 3.8% normal, 4.65% → 5.3% stress). Throughput and memory were unchanged at the experiment's resolution.

For artifact size, both commits were additionally built from the same physical source path. With plugins enabled, observability adds 113,000 B to the unstripped ELF (+0.418%), 78,368 B of .text (+0.392%), 880 B of data, and 528 B of BSS. This is the explicit opt-in diagnostics cost. With plugins disabled, allocated section sizes are identical on both sides (text=19,829,371, data=373,592, bss=3,944) and both stripped files are 20,208,840 B. The full ELFs are not byte-identical because Rust/LLVM codegen identity and layout metadata changed, so the supported claim is zero feature-disabled allocated-size growth rather than binary identity.

Phase 3.2 lifecycle opt-in and binary-size gate

The same release/LTO demo used by the Binary Size workflow was rebuilt from a clean target at updated main and current head 63bb4d95. Baseline → candidate measured 8,066,742 → 8,073,590 bytes of .text (+6,848 B / +6.69 KiB) and 10,089,576 → 10,097,032 stripped bytes (+7,456 B / +7.28 KiB). Before the standalone lifecycle split, the candidate measured 8,105,526 bytes of .text; feature-gating recovered 31,936 B and leaves roughly 25 KiB below the 32 KiB CI budget.

The default client now contains neither the lifecycle field nor its cleanup/startup branches. client-lifecycle restores the standalone API and plugins enables it transitively.

The isolated runtime A/B compares c86f6d52 (before the split) with 63bb4d95 using Cargo paths overrides, prebuilt binaries, separate targets, and five order-alternated pairs on ports 18501–18510. Across five 30,000-ping-pong runs per side, median throughput was 2,859.28/s → 2,859.43/s (+0.005%), client CPU 3.86 s → 3.92 s, RSS average 26.7 → 26.6 MB, HWM 35.5 → 35.0 MB, and anonymous RSS average 10.2 → 10.1 MB. Median host CPU interference was 7.4% → 9.4%, so the 0.06 s client CPU difference is within sampling/interference noise. Each side delivered 150,000 acknowledgements and pongs with zero loss.

The non-LTO harness artifact shrank 87,200 B (-0.325%) and .text shrank 78,380 B (-0.394%). The pre-existing benchmark Cargo.toml and Cargo.lock retained their exact SHA-256 hashes before and after the build/run sequence.

Whole branch after updated-main synchronization

The benchmarked 9e7aba15 head was built against the same dirty-but-unchanged whatsapp-benchs checkout as main 87fbcb55, using Cargo path overrides, prebuilt binaries, and ports 18101–18106; neither benchmark manifest changed. Across three paired 30,000-ping-pong runs at 3,000/s, median actual throughput was 2,859.09/s → 2,859.27/s (+0.006%), median client CPU was 3.93 s → 3.86 s, median RSS average was 27.0 → 26.9 MB, and median HWM was 35.5 → 36.0 MB. Every run delivered all 30,000 acknowledgements and pongs with zero loss. The non-LTO release artifact shrank 194,328 bytes (-0.720%) and .text shrank 168,732 bytes (-0.843%). This shows no measurable hot-path regression after combining the plugin foundations with #1062; the HWM delta is within the observed run-to-run spread. The later 2137fcb5 hardening changes cold authentication/teardown synchronization, handler/subscription destruction, and install-time dependency ownership. Phase 2.2 at its isolated head 9cef55db is benchmarked independently below.

Phase 2.3

The isolated A/B compares 9cef55db with 57ec05dc. Both prebuilt release clients used Cargo source-path overrides and --features whatsapp-rust/plugins without installing a plugin; the external metrics crate therefore proves compile-time/public-API integration without entering the unused runtime path. Ten order-alternated pairs ran on dedicated ports 18401–18420 after AC power was restored. An earlier battery/power-saving batch was discarded in full and is not used here. The pre-existing whatsapp-benchs manifests retained their exact hashes before and after every batch.

Across the ten valid 30,000-ping-pong runs per side, median throughput was 2,859.02/s → 2,859.31/s (+0.01%), client CPU 3.590 s → 3.585 s (-0.14%), RSS average 26.6 → 26.5 MB (-0.38%), HWM 35.25 → 35.05 MB (-0.57%), and anonymous RSS average 10.05 → 10.00 MB (-0.50%). Each side delivered 300,000 acknowledgements and 300,000 pongs with zero loss. Median host CPU interference was 4.45% → 4.50%; the deliberately retained first warm-up pair had higher interference, so medians rather than selected samples are reported.

The release harness artifact changed by +544 bytes (+0.002%); .text changed by +232 bytes, data by +40 bytes, BSS by -232 bytes, and total allocated sections by +40 bytes. These results show no measurable unused-runtime or memory regression from the task clock/API slice.

Phase 2.2

The isolated A/B compares the pre-router head 2137fcb5 with 9cef55db. Both binaries used Cargo source-path overrides, temporary targets and lockfiles, and dedicated ports 18321–18336; the pre-existing whatsapp-benchs Cargo.toml and Cargo.lock hashes remained unchanged.

With plugins disabled, five paired 30,000-ping-pong runs produced median throughput of 2,859.20/s → 2,859.21/s. Three samples showed unrelated host CPU or memory pressure, so resource medians use the clean subset (four baseline and three candidate runs): CPU 3.54 s → 3.64 s, RSS average 26.35 → 26.5 MB, HWM 35.2 → 35.3 MB, and anonymous RSS average 9.9 → 10.1 MB. The non-LTO artifact changed by +9,896 bytes (+0.037%) and .text by +4,988 bytes (+0.025%), despite the feature-disabled code being absent; this is within link/code-layout noise.

The stronger opt-in check rebuilt both sides with Cargo flag --features whatsapp-rust/plugins but installed no plugin. Across three clean paired runs, median throughput was 2,859.29/s → 2,859.33/s, client CPU 3.56 s → 3.37 s, RSS average 26.4 → 26.4 MB, HWM 35.4 → 35.0 MB, and anonymous RSS average 10.0 → 10.1 MB. The candidate artifact was 15,824 bytes smaller (-0.059%) and .text 4,280 bytes smaller (-0.022%). Every one of the 16 runs delivered all 30,000 acknowledgements and pongs with zero loss. The CPU differences are sampling/run-order noise; neither build mode shows a throughput, memory-retention, or unused-runtime regression.

Phase 2.1

The Binary Size check first detected that compiling the native host into default builds added 83.16 KiB stripped and 71.56 KiB of .text. That evidence triggered the intended opt-in feature boundary instead of a budget waiver. A same-toolchain release/LTO demo build at the pre-host head 66f464a9 and the feature-gated head 7c2e584c is now byte-for-byte identical with plugins disabled: 10,112,136 stripped bytes and 8,087,734 .text bytes on both sides. The normal whatsapp-benchs A/B used prebuilt source overrides and separate ports; its Cargo.toml remained untouched. Across three paired runs (30,000 ping-pongs at 3,000/s per side), median actual throughput was 2,859.23/s → 2,859.42/s (+0.01%), sampled client CPU was 3.85 s → 3.70 s, and median HWM was 35.6 → 35.1 MB. Every run delivered all acknowledgements and pongs with zero loss. The non-LTO harness artifact changed by +17,096 bytes (+0.063%); .text changed by +2,368 bytes, consistent with codegen/link-layout noise from the declared opt-in feature rather than a default runtime path.

Phase 1

Phase 1 A/B measurements used isolated copies of whatsapp-benchs against the updated main at cd29cef4, with three normal runs per side:

  • connect median: 5.517 s → 5.518 s (+0.02%)
  • 20 reconnect cycles median: 7.202 s → 7.219 s (+0.24%); every run completed 21 authentications
  • 10,000 ping-pongs median actual rate: 1,817.76/s → 1,817.56/s (-0.01%), with 10,000 acknowledgements, 10,000 pongs, and zero loss in every run
  • DHAT over 20 reconnect cycles: 29,768,739 → 29,318,034 allocated bytes (-1.5%); peak live allocation 2,416,246 → 2,184,867 bytes; live-at-end unchanged at 27,190 bytes
  • release benchmark artifact: +43,624 bytes (+0.162%)

Phase 0 and lifecycle review hardening

The focused A/B compares the pre-Phase-0 head a0da3e2e with 3bd4bd58. Both sides used the same whatsapp-benchs checkout and prebuilt source overrides; the benchmark Cargo.toml was not changed. Normal measurements are medians from three paired runs on separate ports:

  • 30,000 ping-pongs at a 3,000/s rate cap: 2,858.81/s → 2,859.12/s (+0.01%); client CPU 4.40 s → 4.49 s (+0.09 s); HWM 35.7 → 35.9 MB; all six runs delivered every acknowledgement and pong with zero loss
  • 50 reconnect cycles: client CPU 0.37 s → 0.38 s (+0.01 s); HWM 21.3 → 21.3 MB; median RSS growth 7.0 → 6.2 MB and anonymous RSS growth 3.9 → 3.4 MB; all six storms completed
  • DHAT over 50 reconnect cycles: 68,505,259 → 68,360,740 allocated bytes (-0.21%), 391,946 → 393,066 allocation blocks (+0.29%), peak live allocation 2,418,051 → 2,186,598 bytes, and live-at-end unchanged at 27,190 bytes
  • release benchmark artifact: 27,063,904 → 27,082,800 bytes (+18,896 bytes, +0.070%)

The normal-run CPU differences are at sampler resolution and throughput is unchanged. DHAT and equal live-at-end show no lifecycle retention regression.

Validation

Current-head validation additionally passed:

  • cargo fmt --all -- --check and git diff --check
  • cargo clippy --all --tests -- -D warnings
  • cargo test --workspace --exclude e2e-tests (including 1,500 passed / 2 ignored in wacore and 1,269 passed / 1 ignored in whatsapp-rust)
  • all 40 plugin-host unit regressions and the external whatsapp-rust-plugin-metrics crate
  • cargo check -p whatsapp-rust --no-default-features --features plugins
  • WASM: cargo check -p whatsapp-rust --lib --target wasm32-unknown-unknown --no-default-features --features plugins with getrandom_backend=wasm_js
  • five clean final A/B runs per side against current main, with zero lost ack/pong

Focused regressions now also cover manifest-keyed trait-object adapters, owned plugin subscription updates/unsubscribe, immediate registry cleanup after burst churn, capability-bit composition, cooperative connection/install task drains, bounded drain degradation, zero-deadline validation, installation timeout cleanup, and upstream partial-install rollback.

  • Phase 0 regressions: unsubscribe during dispatch, explicit interest widening/narrowing, concurrent registration, two simultaneous raw-forwarding consumers, and final-lease teardown
  • Phase 1 regressions: authentication/teardown races, cancellation-safe cleanup, callback panic isolation, and final close/shutdown ordering
  • cancellation regressions: abort terminal disconnect during transport I/O and abort explicit rollback during a stalled shutdown hook; detached cleanup completes in both cases
  • reentrant teardown regressions: handler Drop may unsubscribe from the same bus or reenter plugin subscription APIs without deadlock, including the post-shutdown rejection path
  • ready-publication regressions: terminal cancellation waits for an in-flight publication, suppresses later Connected, and remains safe when shutdown is signalled from the event handler itself
  • Phase 2.2 regressions: exact selector filtering, bounded overflow/drop accounting, concurrent per-route sequence order, route reset, shutdown drain, and typed API/event vertical slice
  • Phase 2.3 regressions: public-only external metrics plugin, typed API/snapshot/selector, selective core events, install- versus connection-scoped clocks, bounded tick delivery, stale-generation protection, and shutdown
  • Phase 3.1 regressions: generation tasks finish before on_closed, install tasks finish before terminal hooks, rollback waits for task destruction, and task-drain timeout remains bounded
  • Phase 3.3 regressions: shared fanout memory counts once, publisher/router totals remain cumulative, queue drops degrade only the responsible plugin, core-event panics are isolated, delayed handler destruction is contained, and memory-report totals include retained custom-event payloads
  • lifecycle queue regressions: stale Ready admission remains capped, ordinary close ordering is retained, and all 254 queued generation closures are delivered before terminal shutdown
  • reconnect regressions: reconnect() and reconnect_immediately() invoked from on_ready synchronously cancel the active generation and suppress its stale Connected publication
  • lifecycle drop regressions: the final client owner signals a standalone lifecycle, and the obsolete device-registry cleanup task is absent so a fresh client releases its last strong owner without explicit shutdown
  • detached cleanup regressions: a custom transport panic reaches the cleanup waiter instead of leaving it parked forever, and the retired scope is closed on the unwind path
  • terminal-install regressions: standalone lifecycle and plugin install futures that never complete are cancelled within a bound; staged task signals fire synchronously and LIFO shutdown still runs
  • rollback unwind regression: a staged API with a panicking destructor cannot strand the builder waiting for detached cleanup completion
  • resource-teardown regression: a panicking plugin event-handler destructor cannot suppress later plugin shutdown signals or the installed upstream lifecycle
  • tracker-retirement regression: cancelling a connection generation removes its task tracker before on_closed, so bounded lifecycle queue eviction cannot retain stale generations
  • future-teardown regressions: pending plugin and aggregate lifecycle callbacks whose destructors panic are cancelled within an unwind boundary and cannot suppress later hooks or terminal shutdown
  • ownership regression: an API retaining PluginContext is released with the client instead of cycling through the staging registry
  • a lifecycle-rejected authentication success cannot leave is_logged_in set
  • cargo test --all built and linked the complete workspace locally; only the E2E runtime failed because no Bartender mock server was reachable. Pulling the repository-pinned private image remains HTTP 403 with the available GitHub token, so the GitHub E2E job is authoritative.
  • GitHub E2E passed on head 1ee952b9; all checks were green there, including WASM, formatting, clippy, all-features, no-SIMD, binary size, and CodSpeed
  • The complete local suite, all-test Clippy, all 40 plugin-host tests, minimal native/WASM plugin builds, formatting, and diff checks are green on current head 30d06ff6, which contains origin/main at aa2582ca. All review threads are resolved. GitHub checks and AI reviews are rerunning on this head and remain under monitoring.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔄 Running review...
📝 Walkthrough

Walkthrough

Client construction now uses a validated ClientBuilder, clients expose generation-scoped lifecycle hooks, raw-node forwarding uses leases, and event handling uses subscriptions with interest-aware dispatch. Bot wiring, exports, tests, and examples adopt the updated APIs.

Changes

Client platform APIs

Layer / File(s) Summary
Subscription-based event dispatch
wacore/src/types/events.rs
Adds interest-aware subscriptions, aggregate dispatch filtering, unsubscribe/detach operations, and concurrency coverage.
Validated client builder assembly
src/client/builder.rs
Adds dependency validation, durability probing, configurable options, lifecycle installation, controlled service startup, and builder tests.
Connection lifecycle contract
src/client/extension_lifecycle.rs
Adds connection scopes, lifecycle hooks, generation coordination, terminal shutdown, callback handling, and async coverage.
Client lifecycle and forwarding integration
src/client.rs, src/client/lifecycle.rs, src/client/accessors.rs, src/client/node_io.rs
Integrates lifecycle callbacks, async connected-event handling, teardown ordering, lease-based raw-node forwarding, and builder-backed construction.
Bot integration and subscription migration
src/bot.rs, src/lib.rs, src/**, examples/**, tests/**, storages/**
Routes bot construction through ClientBuilder, updates exports, and migrates handlers and tests to subscription APIs.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BotBuilder
  participant ClientBuilder
  participant Client
  participant LifecycleRegistration
  participant ClientLifecycle
  BotBuilder->>ClientBuilder: configure dependencies and options
  ClientBuilder->>Client: build and start services
  Client->>LifecycleRegistration: begin_scope_if_current(generation)
  LifecycleRegistration->>ClientLifecycle: on_ready(scope)
  Client->>LifecycleRegistration: close_scope(generation)
  LifecycleRegistration->>ClientLifecycle: on_closed(scope)
  Client->>ClientLifecycle: shutdown()
Loading

Possibly related PRs

Suggested labels: api-design, breaking-change

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main theme of the PR: adding extensible client and plugin-architecture foundations.
Description check ✅ Passed The description is directly about the same client and plugin-architecture refactor, with phases, validation, and migration notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/plugin-architecture

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.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an extensible native client plugin architecture. The main changes are:

  • Canonical client construction with opt-in lifecycle and plugin features.
  • Generation-scoped plugin lifecycle, tasks, resources, and shutdown handling.
  • Owned core-event subscriptions and reference-counted raw-node forwarding.
  • Bounded custom-event routing with backpressure and delivery statistics.
  • Plugin health, lifecycle, task, and memory diagnostics.
  • Typed and manifest-keyed plugin registration APIs.

Confidence Score: 5/5

This looks safe to merge.

  • Scope replacement now retains displaced generations for closure delivery.
  • Queue pressure keeps mandatory closure callbacks and orders them before shutdown.
  • No remaining blocking issue was found in the updated lifecycle paths.

Important Files Changed

Filename Overview
src/client/extension_lifecycle.rs Adds generation scope management, ordered callbacks, lossless closure delivery, and terminal lifecycle coordination.
src/client/lifecycle.rs Integrates generation cancellation and authoritative cleanup with connection and shutdown flows.
src/client/builder.rs Introduces canonical client assembly, extension installation, activation, and rollback.
src/plugins/mod.rs Defines plugin manifests, registration, capabilities, task ownership, resource tracking, and diagnostics.
src/plugins/events.rs Adds bounded custom-event routing, overflow handling, sequencing, and queue statistics.
wacore/src/types/events.rs Adds owned event subscriptions and aggregate event-interest tracking.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Lifecycle
    participant PluginHost
    participant Plugin

    Client->>Lifecycle: Authenticate generation N
    Lifecycle->>PluginHost: Ready(scope N)
    PluginHost->>Plugin: on_ready(scope N)
    Client->>Lifecycle: Reconnect or disconnect
    Lifecycle->>PluginHost: Cancel scope N
    PluginHost->>PluginHost: Drain generation tasks
    PluginHost->>Plugin: on_closed(scope N)
    Client->>Lifecycle: Terminal shutdown
    Lifecycle->>PluginHost: Shutdown
    PluginHost->>PluginHost: Drain install tasks
    PluginHost->>Plugin: shutdown()
Loading

Reviews (35): Last reviewed commit: "refactor(plugins): leave capability expa..." | Re-trigger Greptile

Comment thread src/client/extension_lifecycle.rs Outdated
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.67 MiB 9.68 MiB +6.97 KiB (+0.07%) 🔺
bin .text 7.74 MiB 7.74 MiB +6.25 KiB (+0.08%) 🔺
bin allocated (text+data+bss) 9.67 MiB 9.68 MiB +8.20 KiB (+0.08%) 🔺
llvm-lines wacore 490,929 492,092 +1,163 (+0.24%) 🔺
llvm-lines wacore copies 16,226 16,249 +23 (+0.14%) 🔺
llvm-lines whatsapp-rust lib 680,827 682,198 +1,371 (+0.20%) 🔺
llvm-lines whatsapp-rust lib copies 21,856 21,846 -10 (-0.05%) 🔽
deps crates (Cargo.lock) 470 471 +1 (+0.21%) 🔺
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.69 MiB 1.70 MiB +4.43 KiB (+0.26%) 🔺
.text wacore 636.30 KiB 636.91 KiB +625 B (+0.10%) 🔺
.text wacore_binary 89.35 KiB 89.35 KiB 0
.text wacore_libsignal 160.96 KiB 160.96 KiB 0
.text wacore_appstate 22.36 KiB 22.36 KiB 0
.text wacore_noise 22.98 KiB 22.98 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 510.58 KiB 510.58 KiB 0
.text whatsapp_rust_tokio_transport 39.84 KiB 39.84 KiB 0
.text whatsapp_rust_ureq_http_client 10.28 KiB 10.28 KiB 0
.text std 951.21 KiB 952.34 KiB +1.13 KiB (+0.12%) 🔺
.text other deps 1.88 MiB 1.88 MiB +47 B (+0.00%) 🔺
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.69 MiB 1.70 MiB +4.43 KiB (+0.26%)
std 951.21 KiB 952.34 KiB +1.13 KiB (+0.12%)

Baseline: aa2582ca7 (latest main run) · Head: 2338e4773 · Graphs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de5d5bc5b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/extension_lifecycle.rs Outdated
Comment thread src/client/extension_lifecycle.rs Outdated
Comment thread src/client/node_io.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib.rs (1)

180-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider wiring the new builder/lifecycle types into prelude too.

prelude still only re-exports Client/ClientError. If plugin hosts and foreign-language adapters are the target audience for ClientBuilder/ClientLifecycle/ConnectionScope, they'll likely want these importable from prelude without reaching into crate::client directly. Not blocking — future phases may handle this deliberately — but worth a look before the plugin API surface locks in.

♻️ Possible addition
 pub mod prelude {
     pub use crate::bot::{Bot, BotBuilder, BotHandle, EventDelivery, MessageContext};
-    pub use crate::client::{Client, ClientError};
+    pub use crate::client::{
+        Client, ClientBuilder, ClientBuilderError, ClientError, ClientLifecycle, ConnectionScope,
+        ConnectionScopeState,
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 180 - 199, Update the prelude module’s client
re-exports to include the new ClientBuilder, ClientLifecycle, and
ConnectionScope types alongside Client and ClientError, making them available
through the standard prelude import path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/lifecycle.rs`:
- Around line 983-985: Update cleanup_connection_state’s lifecycle.close_scope
await so a stuck plugin cannot block reconnect indefinitely: bound the teardown
wait with the existing timeout mechanism or otherwise let reconnect progress
independently of plugin teardown. Preserve normal close_scope execution and
ensure the cleanup path continues after the bound is reached.

---

Outside diff comments:
In `@src/lib.rs`:
- Around line 180-199: Update the prelude module’s client re-exports to include
the new ClientBuilder, ClientLifecycle, and ConnectionScope types alongside
Client and ClientError, making them available through the standard prelude
import path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f76f103-7dbb-471e-9878-767f2302b137

📥 Commits

Reviewing files that changed from the base of the PR and between cd29cef and de5d5bc.

📒 Files selected for processing (8)
  • src/bot.rs
  • src/client.rs
  • src/client/builder.rs
  • src/client/extension_lifecycle.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/lib.rs
  • src/types/enc_handler.rs

Comment thread src/client/lifecycle.rs Outdated
greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/extension_lifecycle.rs`:
- Around line 175-178: Extract a private scopes accessor on the enclosing type
that performs the existing self.scopes lock with poisoned-lock recovery, then
replace the duplicated lock expressions in begin_scope, cancel_active_scope,
close_scope, has_open_scopes, and scope_for with calls to that accessor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 81faaf76-1958-4f71-9d0c-1d18efd298d9

📥 Commits

Reviewing files that changed from the base of the PR and between de5d5bc and 1f28c48.

📒 Files selected for processing (1)
  • src/client/extension_lifecycle.rs

Comment thread src/client/extension_lifecycle.rs Outdated
@greptile-apps
greptile-apps Bot dismissed their stale review July 21, 2026 06:05

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/extension_lifecycle.rs (1)

77-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nail down the public "scope ended" contract before we ship this seam.

is_cancelled() returns true once the state reaches Cancelled or Closed, and a clean close() (Ready→Closed) still fires cancellation_signal because previous < SCOPE_CANCELLED. Internally that's fine and consistent. The problem is this is a public plugin surface — an extension author reading is_cancelled / cancellation_signal will reasonably assume "was cancelled", not "the scope is over". That's the kind of wrong mental model that ships bugs in every plugin built on top of it. Spell the semantics out in the doc comment so people build against what it actually does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/extension_lifecycle.rs` around lines 77 - 111, Update the public
documentation for is_cancelled and the cancellation signal to explicitly state
that they indicate the scope has ended, including both cancellation and clean
closure. Document that close() notifies the cancellation signal even when
transitioning from Ready, so extension authors do not interpret these APIs as
cancellation-only.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/builder.rs`:
- Around line 353-356: Update the lifecycle initialization around
LifecycleRegistration::new to combine the existing Option::map operations into
one closure that constructs and wraps the registration in Arc directly,
preserving the current None behavior and resulting type.

---

Outside diff comments:
In `@src/client/extension_lifecycle.rs`:
- Around line 77-111: Update the public documentation for is_cancelled and the
cancellation signal to explicitly state that they indicate the scope has ended,
including both cancellation and clean closure. Document that close() notifies
the cancellation signal even when transitioning from Ready, so extension authors
do not interpret these APIs as cancellation-only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3058862d-e026-4e71-8f26-bf4933377ba3

📥 Commits

Reviewing files that changed from the base of the PR and between 1f28c48 and a0da3e2.

📒 Files selected for processing (5)
  • src/client/builder.rs
  • src/client/extension_lifecycle.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/lib.rs

Comment thread src/client/builder.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0da3e2e96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/extension_lifecycle.rs Outdated
Comment thread src/client/extension_lifecycle.rs Outdated
Comment thread src/client/lifecycle.rs
@greptile-apps
greptile-apps Bot dismissed their stale review July 21, 2026 11:56

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bot.rs (1)

1284-1314: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the alloc_meter and task_instrument assignments.

Look, we're building the future of messaging here, not a labyrinth of nested matches. This assignment is way more complex than it needs to be. It also completely skips task_instrument if alloc_meter is present. If they aren't mutually exclusive, that's a bug. Either way, clean this up with sequential if let blocks. We need things to work right and be readable.

♻️ Proposed refactor
-        client_builder = match alloc_meter {
-            Some(meter) => client_builder.with_alloc_meter(meter),
-            None => match task_instrument.clone() {
-                Some(instrument) => client_builder.with_task_instrument(instrument),
-                None => client_builder,
-            },
-        };
+        if let Some(meter) = alloc_meter {
+            client_builder = client_builder.with_alloc_meter(meter);
+        } else if let Some(instrument) = task_instrument.clone() {
+            client_builder = client_builder.with_task_instrument(instrument);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bot.rs` around lines 1284 - 1314, Replace the nested
`alloc_meter`/`task_instrument` match assignment on `client_builder` with
sequential `if let` blocks, applying `with_alloc_meter` when `alloc_meter`
exists and independently applying `with_task_instrument` when `task_instrument`
exists. Preserve both options when they are provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/bot.rs`:
- Around line 1284-1314: Replace the nested `alloc_meter`/`task_instrument`
match assignment on `client_builder` with sequential `if let` blocks, applying
`with_alloc_meter` when `alloc_meter` exists and independently applying
`with_task_instrument` when `task_instrument` exists. Preserve both options when
they are provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2b04c62a-ff1d-4c33-a17f-5b93ab3cf2c5

📥 Commits

Reviewing files that changed from the base of the PR and between a0da3e2 and bc16830.

📒 Files selected for processing (24)
  • examples/voip-cli/src/main.rs
  • src/bot.rs
  • src/client.rs
  • src/client/accessors.rs
  • src/client/builder.rs
  • src/client/extension_lifecycle.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/client/tests.rs
  • src/features/labels.rs
  • src/handlers/call.rs
  • src/handlers/notification/mod.rs
  • src/history_sync.rs
  • src/lib.rs
  • src/message/commit_batch.rs
  • src/message/durability.rs
  • src/message/tests.rs
  • src/pair_code.rs
  • src/passkey/flow.rs
  • src/receipt.rs
  • storages/chat-store/src/lib.rs
  • storages/chat-store/src/store.rs
  • tests/e2e/src/lib.rs
  • wacore/src/types/events.rs

@greptile-apps
greptile-apps Bot dismissed their stale review July 21, 2026 12:08

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/extension_lifecycle.rs (1)

306-351: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

after_remove runs while the scopes (and then callback_queue) mutex is held — document that contract.

close_scope_with keeps the scopes MutexGuard alive across the caller-supplied after_remove() closure and the subsequent callback_queue() lock acquisition. Today close_scope always passes || {}, so it's harmless, but the pattern is a live stall risk: any future caller that gives close_scope_with a non-trivial after_remove will block every other synchronous consumer of scopes() (begin_scope_if_current, cancel_scope, ready(), enqueue_shutdown_if_ready) for the duration of that closure. Your own test final_scope_closure_is_published_before_shutdown had to bump to worker_threads = 2 specifically because shutdown()'s cancel_active_scope() synchronously blocks on this held lock — on a single-threaded runtime (e.g. the WASM target mentioned in the PR objectives) this would stall the executor outright.

A short doc comment on close_scope_with spelling out "must be non-blocking; runs while holding the scope/callback-queue locks" would prevent this from becoming a real bug later.

📝 Proposed doc comment
+    /// `after_remove` runs synchronously while holding `scopes` (and then
+    /// `callback_queue`) internally — it MUST be non-blocking. A slow or
+    /// blocking closure here stalls every other lifecycle operation
+    /// (`begin_scope_if_current`, `cancel_scope`, `ready`, `shutdown`) and,
+    /// on single-threaded runtimes, the executor itself.
     fn close_scope_with(self: &Arc<Self>, generation: u64, after_remove: impl FnOnce()) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/extension_lifecycle.rs` around lines 306 - 351, Document the
locking contract on close_scope_with: after_remove must be non-blocking because
it executes while the scopes mutex is held and before or during acquisition of
the callback_queue lock. Place a concise doc comment directly above
close_scope_with and state that callers must not perform blocking or reentrant
operations that require these locks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client/extension_lifecycle.rs`:
- Around line 306-351: Document the locking contract on close_scope_with:
after_remove must be non-blocking because it executes while the scopes mutex is
held and before or during acquisition of the callback_queue lock. Place a
concise doc comment directly above close_scope_with and state that callers must
not perform blocking or reentrant operations that require these locks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a13e8a9d-c01e-4dfd-8e87-860538b85ae8

📥 Commits

Reviewing files that changed from the base of the PR and between bc16830 and 3bd4bd5.

📒 Files selected for processing (2)
  • src/client/extension_lifecycle.rs
  • src/client/lifecycle.rs

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1a8138b77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/builder.rs Outdated
Comment thread src/client.rs
Comment thread src/client/extension_lifecycle.rs Outdated
Comment thread src/plugins/mod.rs Outdated
@greptile-apps
greptile-apps Bot dismissed their stale review July 22, 2026 13:10

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 22, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review July 22, 2026 15:29

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60fda0bce4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/plugins/mod.rs Outdated
@greptile-apps
greptile-apps Bot dismissed their stale review July 22, 2026 15:45

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 22, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review July 22, 2026 15:53

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 22, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review July 22, 2026 16:00

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant