Commit 1eb6b70
* test(binding-mcp): scaffold mcp cache binding IT contract (#1737)
Adds the test-first scaffold for the upcoming mcp cache binding:
McpCacheIT skeleton with all 22 planned test methods (Group A
warmup tests active, Groups B/C/D/F/G/I marked @Ignore until their
scripts land), four IT zilla.yaml configs, schema patch entries
for kind: cache and options.warmup/ttl, and six fully-written
Group A warmup .rpt scripts (lifecycle, tools/list, resources/list,
prompts/list, lifecycle-persists, guarded-credentials).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): add peer-to-peer CacheIT for warmup scripts (#1737)
Adds the missing client.rpt for each Group A cache warmup scenario
and a CacheIT class in the spec project that runs every script pair
peer-to-peer without Zilla. Verifies the scripts are self-consistent
before any cache binding implementation exists.
Verified locally: ./mvnw -pl specs/binding-mcp.spec verify -Dit.test=CacheIT
runs all 6 tests to green.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): split cache ITs by group and add Group B list scripts (#1737)
Splits the monolithic CacheIT/McpCacheIT into per-group classes
(*WarmupIT, *ListIT, etc.) to keep each group focused as the
script set grows. Group A (warmup) renamed to *WarmupIT.
Adds Group B (list operations served from cache) — 4 scenarios:
agent initialize, tools/list, resources/list, prompts/list. Each
scenario carries paired client.rpt (agent at app0) and server.rpt
(cache facade) so CacheListIT can verify the agent↔cache contract
peer-to-peer. McpCacheListIT pairs the agent client.rpt with the
Group A warmup server.rpt at app1 so the cache is populated before
the agent's list arrives. B5 (list-before-warmup) stays @Ignored
with a script TODO.
Verified locally: CacheWarmupIT 6/6 + CacheListIT 4/4 green
peer-to-peer.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): add downstream-error warmup scenario, drop Group D (#1737)
The cache binding is one hop above the proxy; proxy fanout across
exits is already covered by McpProxyIT (shouldList*WithToolkitMulti).
The only cache-specific concern in the original Group D was resilience
to a downstream error during warmup, which is more naturally a Group A
(warmup) scenario than a fanout one.
Adds cache.warmup.session.downstream.error: downstream ABORTs the
tools/list stream during warmup; the lifecycle session must survive
so the cache can continue with other list types or retry later.
CacheWarmupIT 7/7 green peer-to-peer.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): regroup cache ITs by MCP method kind (#1737)
Renames/splits the CacheWarmupIT and CacheListIT pairs (peer-to-peer
and engine-driven) into method-scoped ITs aligned with the issue's
Responsibilities-by-MCP-method table:
CacheLifecycleIT — warmup session open/persist/error/guard,
agent initialize-from-cache
CacheToolsListIT — tools/list warmup + served-from-cache
CacheResourcesListIT — resources/list warmup + served-from-cache
CachePromptsListIT — prompts/list warmup + served-from-cache
Same applies to the McpCache* counterparts. No script files moved;
only the IT-class references changed. All 11 peer-to-peer tests
verified green.
Remaining roster slots (CacheToolsCallIT/ResourcesReadIT/PromptsGetIT
for pass-through invocations, and the per-list-method store/refresh
coverage) will be added as their scripts come online.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): fold cache into mcp.proxy with options.cache (#1737)
Replaces the separate kind: cache binding with options.cache on the
existing kind: proxy binding. Since all original two-binding topologies
have an exact equivalent in the folded model, no expressiveness is lost
and the engine "kind: cache" prerequisite goes away.
Also:
- Renames "warmup" → "hydrate" throughout (more precise terminology
for cache population).
- Flattens the warmup wrapper: authorization/store/ttl now live
directly under options.cache instead of options.cache.warmup.
- Drops the guarded vs. unguarded test split — the .rpt scripts don't
observe authorization on BEGIN so the scenarios produce identical
transcripts.
- Renames IT classes: CacheXIT → ProxyCacheXIT and McpCacheXIT →
McpProxyCacheXIT to align with the existing McpProxyIT neighbour.
- Configs rewritten: cache.yaml/cache.multi.yaml/cache.refresh.yaml
→ proxy.cache.yaml/proxy.cache.multi.yaml/proxy.cache.refresh.yaml;
each declares kind: proxy with options.cache and a stores: memory0
reference.
- Schema patch: drop "cache" from kind enum; replace flat
options.warmup/options.ttl with options.cache {store, ttl,
authorization}; store is required.
Verified: 10/10 ProxyCache*IT peer-to-peer tests pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): flatten cache scenario + method names (#1737)
Renames scenarios and test methods to use "hydrate" / "serve" as
verbs rather than as noun-modifiers. Examples:
cache.hydrate.session.tools.list → cache.hydrate.tools
cache.agent.tools.list.from.cache → cache.serve.tools.list
shouldPopulateToolsViaHydrate → shouldHydrateTools
shouldServeAgentToolsListFromCache → shouldServeToolsList
shouldKeepHydrateSessionOpenAfter... → shouldHydratePersist
Verified: 10/10 ProxyCache*IT peer-to-peer tests pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): add cache refresh scenarios + tests (#1737)
Adds per-method refresh scenarios that model the cache re-issuing a
list call on the same hydrate session after a TTL elapses, plus a
refresh-error case where the refresh attempt aborts and the cache must
retain its prior cached entry.
Scenarios:
cache.refresh.tools / cache.refresh.resources / cache.refresh.prompts
cache.refresh.tools.error
Tests:
ProxyCacheToolsListIT.shouldRefreshTools
ProxyCacheToolsListIT.shouldRefreshToolsError
ProxyCacheResourcesListIT.shouldRefreshResources
ProxyCachePromptsListIT.shouldRefreshPrompts
(engine-driven counterparts added too)
Also renames cache.hydrate.downstream.error -> cache.hydrate.error
(and shouldHydrateDownstreamError -> shouldHydrateError) for the
single-qualifier convention.
Lease-contention coverage (cache.refresh.tools.contended) is
deferred: the lease behavior is store-level, only meaningfully
testable engine-driven via either a TestStore seeding hook or a
multi-worker EngineRule. Both are downstream work from the cache
binding implementation; the wire-level refresh tests above cover
the protocol shape.
Verified: 14/14 ProxyCache*IT peer-to-peer tests pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): add hydrating scenarios per list method (#1737)
Adds three scenarios where the agent's list request arrives while
the cache is still hydrating. The cache facade holds the request
read in full, then waits for a per-method hydrate barrier to fire
before writing the cached response. True timing isn't observable
peer-to-peer, but the scripts document the required wire ordering
for engine-driven tests to enforce.
Scenarios + tests:
cache.serve.tools.list.hydrating shouldServeToolsListHydrating
cache.serve.resources.list.hydrating shouldServeResourcesListHydrating
cache.serve.prompts.list.hydrating shouldServePromptsListHydrating
Verified: 17/17 ProxyCache*IT peer-to-peer tests pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(engine): add entries option to test store for seeded state
Adds TestStoreOptionsConfig with a Map<String,String> entries
field, exposed via the standard options-config builder/adapter
pattern, and wires it through TestStoreContext so each new
TestStoreHandler is pre-populated with the configured entries.
Enables tests to set up store state declaratively before a binding
that uses the store begins operating — e.g., seeding a lease lock
key so a binding observes a held lease and exercises its
already-locked code path deterministically.
Example:
stores:
memory0:
type: test
options:
entries:
tools.lock: "worker-0"
Verified: engine unit tests (320/320), engine.spec ITs (39/39),
and binding-mcp.spec ProxyCache*IT (17/17) all pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): scaffold cache refresh contention test (#1737)
Adds proxy.cache.contended.yaml — a TestStore configured with
tools.lock pre-seeded to a foreign worker id — and a corresponding
McpProxyCacheToolsListIT.shouldRefreshToolsContended test method
(currently @Ignore'd until the cache binding implementation lands).
When enabled, the test verifies that the cache binding consults
the lease before issuing a refresh tools/list: with the lock held
in the store, putIfAbsent returns the seeded value and the
refresh path is skipped. The downstream server.rpt models only
the hydrate exchange, so any spurious refresh tools/list would
hit an unmatched stream and fail the test.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): proper contention scripts with multi-worker engine (#1737)
Replaces the TestStore-seeded contention approach with a more faithful
multi-worker engine test:
- cache.refresh.tools.contended/{client,server}.rpt — models two
hydrate sessions, exactly two tools/list calls on the wire (one
initial hydrate by lease-winner, one refresh by lease-winner).
Second worker's lifecycle is observed but its tools/list never
hits the wire because the lease was lost.
- McpProxyCacheContentionIT — new engine-driven IT class configured
with ENGINE_WORKERS=2 so both cache binding instances genuinely
race for the hydrate / refresh leases against the shared
store-memory. @Ignore'd until cache binding implementation lands.
- ProxyCacheToolsListIT.shouldRefreshToolsContended — peer-to-peer
counterpart added to the existing list IT.
- proxy.cache.contended.yaml dropped — no longer needed (the test
uses proxy.cache.refresh.yaml which already references store-memory).
Verified: 18/18 ProxyCache*IT peer-to-peer tests pass (was 17,
added shouldRefreshToolsContended).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): address review feedback on PR #1774
- proxy.cache.yaml / proxy.cache.refresh.yaml: use binding-level exit
instead of single-element routes for the single-exit case.
- proxy.cache.multi.yaml → proxy.cache.toolkit.yaml.
- Remove explanatory # comments from scripts; scenarios + script bodies
are the documentation.
- Remove class-level comment from McpProxyCacheContentionIT; same
reasoning.
Verified: 18/18 ProxyCache*IT peer-to-peer tests pass.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): fix checkstyle import ordering in McpProxyCacheContentionIT
The static import of ENGINE_WORKERS was placed after java/org statics
with a blank-line separator. Per the project convention static
imports go in a single block alphabetically sorted by full path, so
io.aklivity.* comes first.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): @Ignore engine-driven cache ITs until impl lands
The engine-driven McpProxyCache*IT tests fail at engine bring-up
because the proxy binding does not yet recognize options.cache.
This is the expected test-first state, but CI cannot distinguish
"expected failure until impl" from "regression".
Add class-level @Ignore("TODO: enable when proxy cache option lands")
to all 5 McpProxyCache*IT classes:
McpProxyCacheLifecycleIT, McpProxyCacheToolsListIT,
McpProxyCacheResourcesListIT, McpProxyCachePromptsListIT,
McpProxyCacheContentionIT.
The peer-to-peer ProxyCache*IT tests in specs/binding-mcp.spec
remain active and green — they verify script self-consistency
independent of the binding implementation.
Verified: runtime/binding-mcp clean verify → BUILD SUCCESS, 146
tests run, 5 skipped (the ignored cache ITs).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): add McpCacheConfig POJO + JSON adapter (#1737)
First step of the proxy cache implementation: add the config-layer
types that map to options.cache in the schema. No runtime behavior
yet — McpProxyFactory still ignores the parsed config.
- McpCacheConfig: immutable POJO with store, per-method ttl,
authorization map (guard-name → credentials)
- McpCacheConfigBuilder: fluent builder mirroring the existing
McpAuthorizationConfigBuilder pattern
- McpOptionsConfig: add cache field + 4-arg constructor
- McpOptionsConfigBuilder: add cache() method (nested-builder
pattern matching authorization())
- McpOptionsConfigAdapter: serialize/deserialize the cache block
with ttl + per-guard authorization credentials
Verified: binding-mcp tests pass, checkstyle clean,
binding-mcp.spec SchemaTest still green.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): originate hydrate lifecycle session at bring-up (#1737)
Phase B of the proxy cache implementation. When options.cache is
present, attach() now:
- resolves the unconditional exit route
- allocates a HydrateSession (initialId/replyId etc.)
- schedules a signaler tick that fires immediately
- the tick handler issues a lifecycle BEGIN downstream with
sessionId="hydrate-1", modelled after KafkaGrpcRemoteServerFactory
The session sends reply WINDOW on receipt of the downstream's BEGIN
reply, and END on detach. No list-method enumeration yet; tools/
resources/prompts hydrate, store integration, lease coordination,
and refresh land in later phases.
McpProxyCacheLifecycleIT remains @Ignore'd until all four scenarios
in the class pass; verified locally that the shouldHydrate scenario
itself now runs to green when temporarily un-Ignored.
Also adds store-memory test dependency (the cache configs reference
type: memory for their backing store).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): hydrate per-method list enumeration (#1737)
Phase C — after the hydrate lifecycle reply arrives, the cache now
chains tools/list, resources/list, prompts/list as three sequential
sub-streams on the same hydrate session. Each list stream sends
BEGIN+END (close write), receives BEGIN reply, DATA and END, then
signals the parent HydrateSession to start the next.
Response bodies are discarded for now; store integration arrives in
Phase D.
McpProxyCacheLifecycleIT now passes all 4 tests with the engine
(shouldHydrate, shouldHydratePersist, shouldHydrateError,
shouldServeInitialize) — class-level @Ignore removed.
McpProxyCache{ToolsList,ResourcesList,PromptsList,Contention}IT
remain @Ignore'd until serve-from-cache / refresh / lease land.
Verified: ./mvnw -pl runtime/binding-mcp clean verify → 149 pass,
4 ITs skipped (the still-Ignored cache list / contention ITs).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): persist hydrate response bodies to store (#1737)
Phase D — resolve the configured store at attach() time via
context.supplyStore(resolveId.applyAsLong(cache.store)), thread the
handle through HydrateSession into each HydrateListStream, and on
each list reply END write the accumulated body to the store under
the per-method key ("tools" / "resources" / "prompts") with no
expiry (Long.MAX_VALUE).
Response bodies are buffered byte-by-byte into a per-list-stream
byte[] that grows as needed. On END the accumulated bytes are
decoded as UTF-8 and pushed to the StoreHandler via put().
No serve-from-cache yet — agent list requests still pass through
to the existing proxy code path. Phase E will intercept them and
respond from the store.
Verified: ./mvnw -pl runtime/binding-mcp clean verify → 149 pass,
4 list/contention ITs still @Ignore'd.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): serve list responses from cache (#1737)
Adds McpCacheListServer that intercepts tools/resources/prompts list
streams when options.cache is configured on a proxy binding, looks up
the cached envelope via StoreHandler.get(), and emits the cached bytes
as DATA followed by END without forwarding to upstream.
Un-ignores the hydrate + serve tests across the per-kind cache IT
classes; periodic refresh and hydrating-wait tests remain ignored
pending later phases.
* refactor(binding-mcp): introduce McpListCache; hydrate kinds in parallel (#1737)
Move the per-kind store-key plumbing out of HydrateSession into a new
McpListCache attached to McpBindingConfig, dropping the redundant
cacheStores map. HydrateSession becomes a populator only; the cache
(backed by StoreHandler) is the source of truth for "is kind X ready?"
via an async get. With per-kind status independent of session
sequencing, the three list-stream round-trips dispatch on the same
worker tick - wall-clock hydration drops to a single round-trip, and
an error on one kind no longer delays the others or blocks a retry
on reconnect.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): address PR #1774 review feedback
- Rename `HydrateSession` to `McpHydrateSession` to follow the binding's
type-prefixed inner class convention.
- Make `McpOptionsConfig` constructor package-private; construction is
via `McpOptionsConfig.builder()`.
- Replace the hardcoded `"hydrate-1"` constant in `McpProxyFactory` with
a `Supplier<String>` obtained from `McpConfiguration.sessionIdSupplier()`.
Cache ITs configure `MCP_SESSION_ID_NAME` to a static method returning
`"hydrate-1"`, mirroring the `McpServerIT` override pattern.
- Replace the three flat `Duration ttlTools/ttlResources/ttlPrompts` fields
on `McpCacheConfig` with a nested `McpCacheTtlConfig` (built via
`McpCacheConfigBuilder.ttl()` returning `McpCacheTtlConfigBuilder`).
`McpOptionsConfigAdapter` serializes and deserializes the nested form.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): hoist sessions and hydrate state onto McpBindingConfig
Replace the two factory-level maps on `McpProxyFactory` (`sessions:
Map<String, McpLifecycleServer>` shared across all bindings, and
`hydrateSessions: Long2ObjectHashMap<McpHydrateSession>`) with per-binding
fields on `McpBindingConfig`:
- `sessions: Map<String, McpProxySession>` — now correctly scoped per
binding rather than per worker. Session ids are only meaningful within
one binding's namespace.
- `hydrate: McpProxyHydrate` — the per-binding hydrate session.
`McpProxySession` and `McpProxyHydrate` are package-visible interfaces
declared in `internal.config` so `McpBindingConfig` can type the fields
without depending on the inner classes inside `McpProxyFactory`. The inner
classes implement the interfaces — no behavior change. `McpLifecycleServer`
gains a constructor reference to its `McpBindingConfig` so `cleanup` can
call `binding.sessions.remove(sessionId)`.
This sets up the per-binding hook that the upcoming per-kind factory
extraction will route state through.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): extract McpProxyLifecycleFactory + dispatch table
Move McpLifecycleServer and McpLifecycleClient from McpProxyFactory
into a new McpProxyLifecycleFactory as inner classes - same
enclosing-instance pattern, just a smaller enclosing factory.
McpLifecycleServer (and the cross-class accessors `sender`, `originId`,
`routedId`, `sessionId`, `supplyClient`) is package-private so the
still-inline call/list dispatch in McpProxyFactory can pattern-match
against it. McpLifecycleClient is also package-private because McpClient
and McpListClient hold typed references to it.
Introduce `Int2ObjectHashMap<BindingHandler> factories` on
McpProxyFactory and delegate KIND_LIFECYCLE to the new factory via the
dispatch table; other kinds remain inline until subsequent phases
extract them. Mirrors KafkaClientFactory's per-kind factory pattern.
McpProxyFactory drops 560 lines.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): extract McpProxyItemFactory for tools/call, prompts/get, resources/read
Introduce an abstract McpProxyItemFactory implementing BindingHandler
plus three concrete per-kind subclasses (McpProxyToolsCallFactory,
McpProxyPromptsGetFactory, McpProxyResourcesReadFactory). The base owns
the shared stream state machine - the McpServer and McpClient inner
classes move verbatim out of McpProxyFactory - and exposes three hooks
for the kind-specific bits:
protected abstract int kind();
protected abstract void injectInitialBeginEx(McpBeginExFW.Builder, String sid, String identifier);
protected abstract void injectReplyBeginEx(McpBeginExFW.Builder, String sid, McpBeginExFW upstream);
Each subclass implements ~30 lines: its KIND constant and the
tools-call/prompts-get/resources-read variants of the BEGIN extension
builder. Naming "Item" reflects that the three operations are different
verbs (call/get/read) acting on a single identified MCP item - opposite
to the List kinds.
McpProxyFactory registers all three subclasses in its existing
Int2ObjectHashMap<BindingHandler> factories map next to the Phase 2
McpProxyLifecycleFactory entry; the newStream dispatcher's else branch
collapses to factories.get(kind).newStream(...). The local McpServer,
McpClient, and rewriteReplyBeginEx are removed from McpProxyFactory
along with three now-unused flyweight fields (flushRO, challengeRO,
mcpChallengeExRW) and the McpChallengeExFW import. Net change:
McpProxyFactory shrinks 735 lines (3071 -> 2336); McpProxyItemFactory
adds 1229 lines.
Each subclass receives the same (McpConfiguration, EngineContext,
LongFunction<McpBindingConfig>) constructor signature as the Phase 2
lifecycle factory, and the per-factory do-helpers (doBegin, doData,
doEnd, doAbort, doFlush, doChallenge, doReset, doWindow) are local
copies rather than parent-shared - matches Phase 2 precedent. No
visibility widening was needed beyond what Phase 2 already opened on
McpLifecycleServer/McpLifecycleClient.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(binding-mcp): add MCP_HYDRATE_KIND_FILTER to scope per-kind cache hydrate ITs
The parallel-hydrate path in McpHydrateSession.onBegin iterates over
[KIND_TOOLS_LIST, KIND_RESOURCES_LIST, KIND_PROMPTS_LIST] and dispatches
all three list streams on the same worker tick. The per-kind cache ITs
(McpProxyCache{Resources,Prompts}ListIT.shouldHydrate{Resources,Prompts})
each only `read` their own kind's BEGIN ext, so the other two streams
emit unexpected BEGINs that fail the script assertion. Updating each
script to accept three BEGINs would push parallel-hydrate setup into
tests whose intent is the single-kind hydrate behavior in isolation.
Add MCP_HYDRATE_KIND_FILTER (IntPredicate, default `k -> true`) to
McpConfiguration, exposed as `hydrateKindFilter()`. Mirrors the existing
MCP_SESSION_ID supplier pattern: tests resolve a `Class::method` static
reference; the decoder loads it via MethodHandle. Production keeps the
default — all three kinds hydrate in parallel — so the contention IT
exercises the real behavior unchanged. Each per-kind IT now configures
the filter to its single KIND_*_LIST so only that kind's BEGIN is
emitted and the existing per-kind scripts pass unmodified.
Result locally: 3 hydrate failures fixed
(McpProxyCacheToolsListIT had timed out, also resolved). Three
`shouldServe*` failures remain — cache-serve reads empty payload — and
are independent of hydrate dispatch.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): extract McpProxyListFactory for tools/list, prompts/list, resources/list
Mirror the Phase 3 item-factory shape for the list slice. Introduce an
abstract McpProxyListFactory implementing BindingHandler plus three
concrete per-kind subclasses (McpProxyToolsListFactory,
McpProxyPromptsListFactory, McpProxyResourcesListFactory). The base
owns the shared state machine - McpListClient, McpListClientDecoder
with its ten decode states, McpListServer (passthrough), and
McpCacheListServer (cache-serve variant), plus the indexOfByte JSON
helper - and exposes seven hooks for the kind-specific bits:
protected abstract int kind();
protected abstract void injectInitialBeginEx(McpBeginExFW.Builder, String sid);
protected abstract void injectReplyBeginEx(McpBeginExFW.Builder, String sid);
protected abstract DirectBuffer listReplyOpenPrelude();
protected abstract JsonParserFactory listItemParserFactory();
protected abstract String arrayKey();
protected abstract String idKey();
Each subclass owns its per-kind prelude bytes (the JSON envelope-open
literal), JsonParserFactory for the streaming item parser, array key
("tools" / "prompts" / "resources"), and id key ("name" or "uri" for
resources). The kind field is removed from McpListClient, McpListServer,
and McpCacheListServer - each factory instance is bound to one kind, so
the seven `switch (kind)` blocks in those classes collapse to direct
hook calls.
Cache-vs-passthrough is now dispatched internally in
McpProxyListFactory.newStream: when binding.cache is non-null an
McpCacheListServer is constructed, otherwise an McpListServer with its
McpListClient via lifecycle.supplyClient. The McpProxyFactory factories
map gets three new entries (KIND_TOOLS_LIST / KIND_PROMPTS_LIST /
KIND_RESOURCES_LIST) and the newStream dispatcher collapses to
factories.get(kind).newStream(...) for every dispatched kind, including
lifecycle - the per-kind factories enforce their own session/route
preconditions.
HydrateListStream, McpHydrateSession, and SIGNAL_INITIATE_HYDRATE stay
in McpProxyFactory because they coordinate multi-kind hydrate from one
session and aren't request-time dispatch paths; the small kind-switch
in HydrateListStream.initiate persists.
McpProxyFactory shrinks 1777 lines (2343 -> 566), shedding all list
machinery plus six now-unused do* helpers (doBegin, doData, doAbort,
two doFlush overloads, doChallenge, doReset), unused flyweight RO/RW
fields and imports, and the no-longer-called sessionId(McpBeginExFW)
helper. doEnd, doWindow, and a local newStream remain because
HydrateListStream still calls them. McpProxyListFactory adds 2016 lines.
No further visibility widening was needed beyond what Phase 2 opened
on the lifecycle accessors.
ITs: 156 pass / 3 fail / 8 skipped, identical to the pre-refactor
baseline. The three `shouldServe*` failures are a pre-existing
cache-serve empty-payload bug unrelated to this refactor.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* fix(binding-mcp): pre-seed cache for non-hydrating cache.serve ITs
The three shouldServe*List runtime ITs raced against hydrate: the agent
sent its lifecycle BEGIN, McpProxyLifecycleFactory replied immediately
(it has no hydrate awareness), the agent then sent its list BEGIN, and
McpCacheListServer hit an empty cache because hydrate was still in
flight - the agent received an empty payload.
Make these ITs deterministic by pre-seeding the cache instead of racing
against hydrate. Mid-hydrate is a distinct scenario that lifecycle
gating will cover later (the .hydrating scripts and @Ignore'd
shouldServe*ListHydrating methods are removed here; when lifecycle
gating lands we'll re-introduce a clearer cache.serve.<kind>.refreshing
flavor rather than overload "hydrating").
Changes:
- New config proxy.cache.seeded.yaml using TestStore with options.entries
carrying the tools/resources/prompts JSON. The existing proxy.cache.yaml
is unchanged - hydrate ITs continue to verify an empty memory store.
- Runtime shouldServe{Tools,Resources,Prompts}List ITs switch to
proxy.cache.seeded.yaml and reuse the existing cache.hydrate/server
script for the downstream (it only handles the hydrate-1 lifecycle
accept + reply, which is exactly what a fully-cached binding produces
on the wire when McpHydrateSession.onBegin sees every cache.get
returning non-null and spawns no list streams).
- Delete the three .hydrating script directories and the @Ignore'd
shouldServe*ListHydrating test methods (both runtime and peer ITs).
Runtime: 156 pass / 0 fail / 5 skipped (was 156p / 3f / 8s).
Peer: 15 pass / 0 fail / 0 skipped.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): gate agent lifecycle BEGIN reply on hydrate complete
When proxy.cache is configured, the agent's lifecycle BEGIN reply now
waits until the hydrate session has settled every kind permitted by
MCP_HYDRATE_KIND_FILTER. By the time an agent's lifecycle is
"initialized" from its point of view, every list method served by this
binding is in the cache - subsequent tools/list, resources/list,
prompts/list requests get a synchronous cache hit and never race the
hydrate path. Bindings with no cache configured retain the previous
synchronous BEGIN-reply behavior (binding.hydrate == null branch).
Mechanism uses the engine's existing signal infrastructure:
- McpHydrateSession tracks totalKinds (computed from the filter) and
settledKinds. Each kind settles either from cache.get returning
non-null at hydrate startup (pre-seeded store) or from the matching
HydrateListStream's terminal write (cache.put completion, bodyLen==0
skip-put, or abort/reset). Once settledKinds == totalKinds, the
session flips the complete latch and fires signaler.signalNow against
every pending agent stream registered via awaitComplete.
- McpProxyHydrate gains awaitComplete(originId, routedId, streamId,
traceId, signalId). When complete is already true the signal fires
immediately (synchronous fast path for the pre-seeded case); otherwise
the (streamId, signalId) pair is queued. cleanup discards pending
before tearing the session down.
- McpProxyLifecycleFactory.McpLifecycleServer.onServerBegin now
transitions initial state and emits WINDOW synchronously, then
delegates the BEGIN reply via binding.hydrate.awaitComplete using the
agent's reply-id as the signal target (the engine registers the
binding's stream consumer in throttles[replyId], so SignalFW for that
stream id routes back to onServerMessage). A new SignalFW case
dispatches to onServerSignal, which fires doDeferredServerBegin for
SIGNAL_HYDRATE_COMPLETE. The deferred path guards on
replyOpened/replyClosed in case the agent abandoned before complete
fired, and rebuilds beginEx inside the deferred path so the shared
codecBuffer is fresh at fire time.
- HydrateListStream now holds a back-reference to its parent
McpHydrateSession (passed via the constructor) and a settled flag to
dedupe completion when both onEnd and abort/reset fire on the same
stream.
McpProxyCacheLifecycleIT.shouldServeInitialize switches to
proxy.cache.seeded.yaml: an empty memory store would block forever
because the hydrate session's three list streams find no cooperating
downstream in cache.hydrate/server. With the pre-seeded test store the
hydrate session sees every cache.get return non-null synchronously, the
complete latch flips before the agent's lifecycle arrives, and the
deferred BEGIN reply takes the synchronous fast path.
Runtime: 156 pass / 0 fail / 5 skipped (was 156p / 3f-then-0f after
gating with the wrong streamId).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): periodic refresh per kind on TTL elapse
McpHydrateSession now uses signaler.signalAt to re-issue list-kind
hydrate streams when the per-kind TTL elapses. Scheduling fires:
- After each kind's initial hydrate completes (HydrateListStream
reaches its terminal: cache.put completion, bodyLen==0 skip-put, or
abort/reset — same gating dedupe applies, so each instance schedules
at most one next refresh).
- After each cache.get returns non-null at startup (pre-seeded value
still refreshes on TTL, so the value stays fresh).
- After each refresh stream's terminal (recursive, same code path).
Each kind has its own signal id (SIGNAL_REFRESH_{TOOLS,RESOURCES,
PROMPTS}) and TTL (McpCacheTtlConfig.tools / .resources / .prompts).
When ttlForKind returns null (no cache.ttl block, or the block omits
that kind), no refresh is scheduled - the cache value stays forever.
On abort/reset of an in-flight refresh, the cache.put never fires, so
the prior cached value is preserved (the test that validates this
behaviour is now un-ignored as shouldRefreshToolsError).
McpHydrateSession.settle takes the kind explicitly and both contributes
to the initial-hydrate gating latch and schedules the next refresh.
HydrateListStream forwards its kind into parent.settle(kind) - no
behavioural change to the gating dedupe.
McpProxyCache{Tools,Resources,Prompts}ListIT.shouldRefresh* and
shouldRefreshToolsError no longer @Ignore - now run against
proxy.cache.refresh.yaml (ttl tools=PT1S, resources=PT2S, prompts=PT3S)
via the existing cache.refresh.<kind> / cache.refresh.<kind>.error
spec scripts. Verified: 156 pass / 0 fail / 1 skipped (Contention IT
class-level @Ignore for lease coordination remains).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): cache hydrate session emits authorized BEGINs to downstream
When options.cache.authorization is configured, McpProxyFactory.attach
resolves the single configured guard via context.supplyGuard and calls
guard.reauthorize(traceId, binding.id, 0L, credentials) once to mint a
session token. The McpHydrateSession and HydrateListStream now stamp
that token onto every outbound BEGIN/END/WINDOW in place of the prior
hardcoded 0L authorization, so any downstream binding that runs a guard
recognises the cache hydrate session as an authenticated principal.
The map is constrained to a single guard entry by the schema patch
(maxProperties: 1 under cache.authorization); the runtime picks that
entry via iterator without defensive assertion, since invalid yaml is
rejected at config-load time.
Token freshness is left to the guard for now - the binding does not
re-reauthorize per BEGIN. If a future guard implementation needs
short-lived tokens, the call site is a single line and can move to
per-stream resolution without changing the API shape.
New test scaffolding:
- specs/.../config/proxy.cache.auth.yaml - engine test_guard +
test store pre-seeded with empty list payloads (so hydrate spawns
no list streams; the authorization assertion lands on the lifecycle
BEGIN alone) + proxy declaring cache.authorization.test_guard with
matching credentials.
- specs/.../streams/application/cache.hydrate.auth/{client,server}.rpt
mirrors cache.hydrate/ but adds option zilla:authorization 1L (the
TestGuard's first reauthorize result) to assert the proxy stamps the
expected long on its hydrate BEGIN.
- shouldHydrateAuth peer IT in ProxyCacheLifecycleIT and engine-driven
IT in McpProxyCacheLifecycleIT.
Verified: spec ProxyCache*IT 16 pass / 0 fail; runtime binding-mcp
157 pass / 0 fail / 1 skipped (was 156p/0f/1s; +1 shouldHydrateAuth).
McpProxyCacheContentionIT remains the only @Ignore'd test pending
lease coordination.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* feat(binding-mcp): putIfAbsent lease coordination for hydrate + refresh
When two workers attach the same proxy-cache binding, both would
otherwise race to issue every list call on the wire. Coordinate via
the configured store's putIfAbsent so exactly one worker issues each
call.
Two leases, one shared key namespace on the configured store:
- Binding-wide "lifecycle.lock": acquired before opening the hydrate
lifecycle stream to downstream. Loser polls every 100ms until the
lock frees; winner holds it through the initial hydrate. Released
inside markComplete once every kind permitted by the filter has
settled, so other workers can then open their own lifecycle
(independent stream onward) and observe the now-populated cache.
- Per-kind "<kind>.lock": acquired before issuing a HydrateListStream
(both initial and refresh paths). Loser at initial-hydrate time
markSettled-s without scheduling a refresh - the lease holder owns
refresh scheduling because settle (gated by the per-stream settled
flag) is the only path that calls scheduleRefresh. Released after
cache.put completes (or after onEnd's skip-put branch, or after an
abort/reset terminal) and before parent.settle fires.
settle is split into markSettled (gating-latch only) and settle
(markSettled + scheduleRefresh). cache hits at startup, lease losses
at initial hydrate, and lease losses at refresh-time all funnel
through markSettled so they contribute to lifecycle gating without
arming a refresh signal on a worker that didn't actually do the work.
McpProxyCacheContentionIT (was class-level @Ignore'd) is now enabled,
configured with MCP_HYDRATE_KIND_FILTER restricting to KIND_TOOLS_LIST
(since cache.refresh.tools.contended/server.rpt only models the tools
exchanges; resources and prompts are not in scope for this scenario).
hydrateSessionId now cycles "hydrate-A"/"hydrate-B" per call so the
two workers' attach-time calls match the script's session ids.
Lease TTLs:
- lifecycle: 30s (safety net for crashes; explicit release covers the
happy path).
- per-kind: 30s ditto.
- Retry polling for lifecycle: 100ms.
Verified: 157 pass / 0 fail / 0 skipped. Every test that was
@Ignore'd at the start of the PR is now active and green.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): rename HydrateListStream to McpHydrateListStream
Type-prefixed inner class naming convention matches the rest of the
file (McpServer, McpClient, McpListServer, McpCacheListServer,
McpLifecycleServer, McpLifecycleClient, McpHydrateSession).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* test(engine): share TestStore entries across workers via per-Store map
TestStoreHandler previously held its own HashMap<String, String>
per-handler, so workers running in the same engine each saw an
isolated copy of the configured entries (putIfAbsent from one worker
was invisible to another). Lease coordination in binding-mcp's cache
hydrate path relies on cross-worker putIfAbsent visibility, so move
the entries map up one level: TestStore now owns a
ConcurrentMap<Long, ConcurrentMap<String, String>> keyed by storeId,
and TestStoreContext.attach hands out a reference to the per-storeId
shared map to each worker's TestStoreHandler.
Seeding via options.entries uses putIfAbsent so the first worker's
attach is the one that loads the map; subsequent attaches see the
existing entries and skip. No TTL behaviour was added — production
binding code releases leases explicitly, and the existing skip-TTL
semantics is sufficient for the cache test scenarios.
binding-mcp switches its three remaining memory-store configs
(proxy.cache.yaml, proxy.cache.refresh.yaml, proxy.cache.toolkit.yaml)
to type: test, matching the dependency hygiene rule that
implementations must not depend on other implementations - the cache
ITs now run entirely against the engine's test-jar store. The
store-memory test dependency drops out of runtime/binding-mcp/pom.xml.
Verified: 157 pass / 0 fail / 0 skipped (all cache ITs, McpServerIT,
McpClientIT, McpProxyIT, McpProxyCacheContentionIT) and the engine
spec IT suite (39 / 0 / 0) — no regression in either project from
the TestStore sharing change.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): extract McpCacheHydrater from McpProxyFactory
McpProxyFactory had grown to ~566 lines absorbing the proxy-cache
feature; the actual dispatch (parse BEGIN ext, look up by kind, delegate
to a per-kind factory) was a small slice of that. The rest -
McpHydrateSession, McpHydrateListStream, the SIGNAL_INITIATE_HYDRATE /
SIGNAL_REFRESH_* / LEASE_TTL_MS / LEASE_RETRY_MS constants, the
PendingAwait record, the local newStream/doEnd/doWindow helpers, the
cache attach wiring (store resolve, McpListCache instantiate,
cacheGuard reauthorize, McpHydrateSession construct, schedule
SIGNAL_INITIATE_HYDRATE), and the matching detach cleanup - is now
owned by a new sibling McpCacheHydrater.
McpCacheHydrater exposes attach(McpBindingConfig) and
detach(McpBindingConfig) mirroring the BindingHandler lifecycle the
engine drives. McpProxyFactory constructs one in its ctor and delegates
both calls.
McpProxyFactory drops from 795 lines to 136 (a pure dispatcher now):
keeps the factories map and its seven per-kind registrations, the
bindings map, supplyGuard for McpBindingConfig construction,
mcpBeginExRO/beginRO for parsing the inbound BEGIN to extract the
kind, and the new hydrater field.
McpCacheHydrater is 724 lines holding everything cache-hydrate: signal
ids, lease ttls/retry, PendingAwait record, McpHydrateSession (still
implementing McpProxyHydrate so McpProxyLifecycleFactory can defer the
agent BEGIN reply via awaitComplete), McpHydrateListStream (settle /
cache.put / lease release path unchanged), and per-worker flyweights
duplicated for the hydrate stream-write helpers. The hydrate session
keeps its own codecBuffer (separate UnsafeBuffer allocated against
writeBuffer.capacity), matching the per-handler flyweight pattern.
McpBindingConfig gains one accessor: long resolveId(String name) plus
the corresponding private ToLongFunction<String> field initialized
from binding.resolveId. Needed because McpCacheHydrater.attach
receives only the wrapped McpBindingConfig and must resolve the cache
store name to a binding id at attach time. Cleaner than passing the
raw BindingConfig alongside.
Verified: 158 pass / 0 fail / 0 skipped (identical to pre-refactor
baseline). Checkstyle 0 violations. JaCoCo coverage met (bundle grew
116 -> 117 classes for the new McpCacheHydrater).
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): per-kind cache hydrater hierarchy
Address PR #1774 review comments:
- #1 cache instantiation moves into McpBindingConfig ctor (supplyStore
becomes a third constructor parameter, parallel to supplyGuard).
- #2 PendingAwait -> McpSignalHandle as a standalone package-private
record with signalVia(Signaler) convenience method; lives in its own
file in the stream package.
- #3 acquire-lifecycle-lease callback is a named method reference
rather than an inline lambda; traceId is re-supplied inside the
method body via supplyTraceId.getAsLong() instead of captured.
- #4 doLifecycleBegin guard inverted to wrap the body, single return
per method.
- #5 separate signal handlers per kind; initial population is the first
refresh. The binding-level orchestrator (McpProxyCacheHydrater) owns
the lifecycle stream and tracks readiness via a populated counter
rather than a totalKinds/settledKinds pair on a McpHydrateSession.
Per-kind work moves into McpProxyCacheListHydrater (abstract base)
with three concrete subclasses (McpProxyCacheToolsListHydrater /
ResourcesListHydrater / PromptsListHydrater), mirroring the
McpProxyListFactory hierarchy.
- #6 cleanup's inline doEnd / state update is consolidated into a
standard doInitialEnd helper on McpProxyCacheHydrater.
Kind and signal id are passed via the abstract base's constructor as
protected final fields rather than abstract methods - only the truly
kind-specific behaviour (injectInitialBeginEx and ttl()) remains
abstract. Each concrete subclass collapses to ~15 lines.
McpProxyHydrate interface and the old McpCacheHydrater are deleted.
McpProxyLifecycleFactory now calls binding.hydrater.register(handle)
directly with an McpSignalHandle instance instead of going through an
interface.
McpListCache stays at its current public surface (get / put /
acquireLease / releaseLease / acquireLifecycleLease /
releaseLifecycleLease) - the readiness tracking lives on the
orchestrator because it already knows the filtered kind set from
hydrateKindFilter at attach time, so no expecting(kind) plumbing was
needed.
Files:
- McpProxyHydrate.java deleted
- McpCacheHydrater.java deleted (replaced)
- McpProxyCacheHydrater.java new - per-binding orchestrator
- McpProxyCacheListHydrater.java new - abstract base + 3 subclasses
- McpSignalHandle.java new
- McpBindingConfig.java - supplyStore ctor param, cache field
initialised in ctor, hydrate field renamed to hydrater and retyped
- McpProxyFactory.java - per-binding hydrater instantiation in attach
- McpProxyLifecycleFactory.java - binding.hydrater.register instead of
binding.hydrate.awaitComplete
- McpClientFactory.java - 1-line ctor signature update for the now
3-arg McpBindingConfig ctor
Verified: 158 pass / 0 fail / 0 skipped, identical to baseline.
Checkstyle 0 violations.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): split per-kind subclasses into top-level files
Address PR #1774 review comments asking that each concrete per-kind
subclass living as a package-private sibling next to its abstract base
be moved to its own top-level file. Three files affected, nine new
files created:
McpProxyItemFactory.java (1229 -> 1124) loses three subclasses:
- McpProxyToolsCallFactory.java
- McpProxyPromptsGetFactory.java
- McpProxyResourcesReadFactory.java
McpProxyListFactory.java (2016 -> 1819) loses three subclasses:
- McpProxyToolsListFactory.java
- McpProxyPromptsListFactory.java
- McpProxyResourcesListFactory.java
McpProxyCacheListHydrater.java (329 -> 257) loses three subclasses:
- McpProxyCacheToolsListHydrater.java
- McpProxyCacheResourcesListHydrater.java
- McpProxyCachePromptsListHydrater.java
Each new file has the standard Aklivity Community License header, a
minimal import set (alphabetical within groups), and contains only the
final class declaration extending the base. Base files trim imports
that were only used by the lifted subclasses (Map and StreamingJson
out of McpProxyListFactory; KIND_TOOLS_LIST / KIND_RESOURCES_LIST /
KIND_PROMPTS_LIST static imports out of McpProxyCacheListHydrater).
No visibility widening was needed - subclasses already touched only
protected abstract hooks on the bases or package-private fields on
parent objects.
Verified: 158 pass / 0 fail / 0 skipped. Checkstyle 0 violations.
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): collapse options.cache.ttl to a single Duration
Drop McpCacheTtlConfig and its three per-kind fields (tools, resources,
prompts) in favour of a single Duration on McpCacheConfig. No real-world
signal that the per-kind knobs are needed; can be reintroduced additively
if/when a use case appears. Default lives in the JSON schema (PT5M), with
the adapter mirroring it as fallback when ttl is omitted.
* fix(binding-mcp): reschedule cache refresh when lease lost
onRefreshAcquireLeaseComplete previously only acted on the lease winner;
losers stopped refreshing forever, leaving the cluster dependent on a
single worker's timer. Mirror the initial-acquire path: losers reschedule
their refresh so every worker stays armed and re-attempts on the next tick.
* refactor(binding-mcp): pass EngineContext to McpBindingConfig
Replace the (BindingConfig, LongFunction<GuardHandler>,
LongFunction<StoreHandler>) ctor and its 1-arg sugar with a single
(BindingConfig, EngineContext) ctor. McpBindingConfig pulls supplyGuard
and supplyStore from the context directly, and the null-guards on the
suppliers disappear because every caller now passes a real context. The
supplier fields on McpProxyFactory and McpClientFactory go with it -
they only existed to be passed through. Bind options.authorization and
options.cache to locals so the long null-chains collapse.
* refactor(binding-mcp): address PR #1774 review batch (phases A–M)
- Rename MCP_HYDRATE_KIND_FILTER → MCP_HYDRATE_FILTER; expose lease
ttl/retry as MCP_LEASE_TTL_MS / MCP_LEASE_RETRY_MS configuration
properties (phases A, B)
- Split McpListCache into per-kind instances; introduce McpLifecycleCache
for lifecycle distributed lock; drop kind parameter from cache ops
(phase C)
- McpBindingConfig owns per-kind caches (toolsCache, resourcesCache,
promptsCache, lifecycleCache) and the hydrater; Optional chains for
guard/store resolution; McpProxyFactory attach/detach simplified
(phases D, G)
- Extract McpHydrateLifecycleStream inner class; guard-then-route
ordering; complete End/Abort/Reset state gating; all fields final
(phase E)
- Decouple McpProxyCacheListHydrater from parent reference; pass
EngineContext + LongSupplier supplyAuthorization directly; rename
onMessage → onListHydrateMessage (phase F)
- McpProxyLifecycleFactory: rename doDeferredServerBegin →
doServerBeginDeferred; invert early-return guard (phase H)
- McpProxyItemFactory: rename builder parameter b → builder in abstract
signatures; update subclasses (phase I)
- McpProxyListFactory: replace abstract kind() with final int kind
field; make sessionId() abstract; move JsonParserFactory construction
to base class constructor accepting List<String> pathIncludes;
remove listItemParserFactory() abstract method (phases J, K)
- IT helpers: extract sessionId() and hydrateXxxOnly() static helpers;
update configure() calls to method-reference form (phase L)
- proxy.cache.seeded.yaml: convert embedded JSON to YAML block scalars
(phase M)
https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8
* refactor(binding-mcp): address PR #1774 review batch
- Rename MCP_LEASE_TTL_MS/RETRY_MS to MCP_LEASE_TTL/RETRY as Duration
properties with ISO-8601 defaults (PT30S, PT0.1S)
- Extract list hydrate stream into inner class McpListHydrateStream with
ExpandableArrayBuffer body accumulator
- Add abstract signalId() override on each list hydrater subclass instead
of passing signalId as constructor parameter
- Replace String prefix with String8FW in McpRoutePrefix to eliminate
per-stream byte allocation in list factory
- Remove kind() abstract method from item/list factories; pass kind as
constructor parameter and store as final field
- Move sessions map initialization into McpBindingConfig constructor
https://github.com/aklivity/zilla/pull/1774
* feat(engine): add Signaler.signalAt(Instant) overloads
Provides Instant-accepting default methods that delegate to the existing
long-based overloads via toEpochMilli(). Required by binding-mcp cache
hydrater for cleaner lease retry scheduling.
* refactor(binding-mcp): address PR #1774 review batch
- McpListCache.acquire and McpLifecycleCache.acquireLifecycle now take
Duration instead of long milliseconds; conversion happens internally
- Drop "Lease" from on*AcquireComplete callback names on list hydrater
- Move cacheGuard and cacheCredentials onto McpLifecycleCache as
guard/credentials fields; McpBindingConfig no longer exposes them
- Split lifecycle and list hydrate stream constructors from their
behavioral work; creators explicitly call doListHydrateBegin/End and
doLifecycleBegin post-construction
- Add proper onListHydrateAbort/Reset handlers with do* helpers and
McpState.initialClosed/replyClosed gating
- Use Signaler.signalAt(Instant) overloads with Instant.now().plus(...)
for lease retry and cache refresh scheduling
https://github.com/aklivity/zilla/pull/1774
* test(engine): cover Signaler.signalAt(Instant) default methods
Default methods in interfaces emit bytecode in the interface class, so
adding them shifts Signaler from "no instructions" to "uncovered class"
in JaCoCo's view. Cover both overloads via a stub Signaler that captures
the delegated timeMillis to keep the engine's missed-class count under
its threshold.
* refactor(binding-mcp): reuse McpAuthorizationConfig under cache
McpCacheConfig.authorization changes from Map<String, String> to
McpAuthorizationConfig with an optional credentials field. Schema is
unchanged (options.cache.authorization remains a single-entry guard-name
map at the YAML level); the adapter now flattens the single map entry
into the McpAuthorizationConfig fields on read and back to a single
entry on write.
The Map.Entry/Iterator dance in McpBindingConfig drops out — the cache
guard and credentials read directly off the McpAuthorizationConfig.
Note: at runtime there is no overlap between options.authorization
(server/client kinds) and options.cache.authorization (proxy kind), so
the type reuse is purely structural — credentials remains unused at the
top-level site for now. A future change to the mcp client binding may
use the top-level credentials to lay out a Bearer authorization header.
https://github.com/aklivity/zilla/pull/1774
* refactor(binding-mcp): self-target binding for multi-route hydrate
The hydrater previously sent its lifecycle and per-kind list streams
directly to a single resolved route, so the cache only captured one
route's data on multi-route bindings. Now the hydrater self-targets
binding.id, letting the binding's own dispatch fan out across all
routes via McpProxyListFactory's McpListServer aggregation path —
the same code path that serves uncached agent list calls.
- McpProxyCacheHydrater: routedId = binding.id (self-loop); drop
pre-resolution and the `enabled` field; lifecycle acquired
unconditionally on start.
- McpProxyListFactory.newStream: detect self-loop
(originId == routedId) and skip the cache server, falling through
to McpListServer aggregation so the hydrater observes the merged
result rather than serving from its own empty cache.
- McpProxyLifecycleFactory.onServerBegin: when self-loop, bypass the
hydrate-complete gating that otherwise defers the lifecycle reply
BEGIN — the hydrater's own lifecycle would deadlock waiting for
itself.
The 8 spec-driven cache ITs that model the old direct-to-app1 wire
shape are temporarily @Ignored — the new lazy-via-supplyClient wire
shape requires re-authoring the corresponding `app1`-side server.rpt
scripts, follow-up commit.
https://github.com/aklivity/zilla/pull/1774
* refactor(binding-mcp): mechanical review cleanups
- McpBindingConfig: keep cacheAuth as Optional<McpAuthorizationConfig>
and derive cacheGuard/cacheCredentials via map chains, instead of
unwrapping early.
- McpProxyCacheListHydrater.onListHydrate{Abort,Reset}: drop the
state-already-closed guards before McpState.closedReply/closedInitial
setters — the bitmask is idempotent and the do{Abort,Reset} helpers
carry their own closed-state guards.
- McpProxyCacheListHydrater.onListHydrateEnd: use
bodyBuffer.getStringWithoutLengthUtf8 instead of allocating a new
String from the byte array.
https://github.com/aklivity/zilla/pull/1774
* refactor(binding-mcp): defer list hydrate END to initial window
doListHydrateBegin now marks the initial stream as closingInitial
after sending BEGIN. The reactive doListHydrateEnd happens on receipt
of the initial WINDOW frame — when initialClosing && !initialClosed,
send END. This avoids the proactive END from the call site of the
constructor and follows the standard close-on-window pattern.
https://github.com/aklivity/zilla/pull/1774
* refactor(engine): remove default modifier on Signaler.signalAt(Instant)
Per review feedback, the Signaler.signalAt(Instant, ...) overloads are
now abstract — implementations provide them directly rather than
inheriting a default that delegates via toEpochMilli().
- EngineWorker.EngineSignaler: convert via toEpochMilli at the entry
point, delegating to the long-based variant.
- KafkaClientConnectionPool.KafkaClientSignaler: delegate the simple
variant to its inner Signaler; convert the stream variant via
toEpochMilli.
- TlsWorker.TlsSignaler (test bench): same toEpochMilli conversion.
- Drop SignalerTest — it existed only to keep JaCoCo's missed-class
count under threshold for the default-method bytecode; with abstract
methods Signaler has no executable code to cover.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): re-enable seeded-mode cache serve ITs
With the self-target hydrate, a seeded cache no longer fans out to
the downstream — no app1 traffic at hydrate time. Drop the
\`cache.hydrate/server\` spec from the four seeded-mode tests
(shouldServe{Initialize,ToolsList,ResourcesList,PromptsList}) and
keep only the agent-side client spec. The script root override
(@ScriptProperty serverAddress=app1) drops out alongside since the
remaining client spec targets app0 by default.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): drop redundant shouldHydrate IT
In the new self-target hydrate model, shouldHydratePersist already
exercises the full hydrate flow (lifecycle + all three list kinds),
making shouldHydrate (which only modelled lifecycle direct to the
downstream) redundant.
Three cache hydrate ITs remain @Ignored — shouldHydrateError,
shouldHydrateAuth (McpProxyCacheLifecycleIT), and
shouldRefreshToolsContended (McpProxyCacheContentionIT) — each
needs its app1-side spec script re-authored against the new lazy
fan-out wire shape.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): rename shouldHydratePersist to shouldHydrate
The original shouldHydrate scenario only handled the lifecycle BEGIN
handshake at app1, leaving the per-kind list streams that the proxy
opens unmatched — the script was structurally incomplete and only
passed in the legacy direct-route hydrate model because the proxy
wasn't in the path to surface the unhandled streams.
shouldHydratePersist already covered the substantive contract: one
hydrate lifecycle is opened and reused across all three per-kind
list hydrate streams (asserted structurally by the single
'accepted' block for lifecycle vs three for the list kinds).
Promote shouldHydratePersist to shouldHydrate by replacing the
cache.hydrate/{client,server}.rpt scripts with the persist
versions, removing the now-redundant cache.hydrate.persist
directory, and renaming the test methods in McpProxyCacheLifecycleIT
and the peer-to-peer ProxyCacheLifecycleIT.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): re-author cache hydrate scenarios for self-target
Update spec scripts to match the wire shape produced by the proxy's
multi-route hydrate via its own dispatch. All three list-kind streams
arrive at app1 (lifecycle plus tools/list, resources/list, prompts/list),
not just one — the previously incomplete scripts only happened to pass
because the legacy direct-route hydrate did not surface them through
McpListServer at app0.
- cache.hydrate.error: extend server.rpt and client.rpt to handle all
three list kinds; tools aborts, resources and prompts succeed so the
remaining caches still populate.
- cache.refresh.tools.contended: simplify to one lifecycle plus two
tools/list streams (initial + refresh); the per-kind lease ensures
only one worker's call reaches app1 per round.
- Rename proxy.cache.auth.yaml to proxy.cache.credentials.yaml and
cache.hydrate.auth/ scripts to cache.hydrate.credentials/; rename
shouldHydrateAuth to shouldHydrateWithCredentials in both ITs.
All 157 engine-driven binding-mcp ITs and the spec peer-to-peer ITs
pass; the three previously @Ignored scenarios are now active.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): drop redundant per-kind hydrate scenarios
cache.hydrate.tools / cache.hydrate.resources / cache.hydrate.prompts
were subsets of cache.hydrate (multi-kind): each only asserted
"lifecycle BEGIN + one kind's list BEGIN", which the multi-kind
scenario already verifies. The lifecycle-reuse property is structural
(McpLifecycleClient.supplyClient with computeIfAbsent) and is
exercised by the multi-kind scenario.
Drop the three per-kind hydrate script directories and the
shouldHydrateTools / shouldHydrateResources / shouldHydratePrompts
test methods from both the engine-driven and peer-to-peer ITs. The
per-kind ITs keep their shouldServe* and shouldRefresh* methods,
which still benefit from MCP_HYDRATE_FILTER scoping the refresh
path to one kind.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): cache hydrate across multiple toolkit routes
Add shouldHydrateToolkit on McpProxyCacheLifecycleIT — runs against
proxy.cache.toolkit.yaml (two routes: bluesky → app1, quartz → app2)
and verifies the hydrater fans out to both downstream routes via the
binding's own McpListServer aggregation, with each route returning
its own data and McpListServer merging into a single cached value.
cache.hydrate.toolkit/server.rpt has eight accept blocks — one
lifecycle plus three list streams per downstream route — capturing
the full wire shape that distinguishes multi-route from single-route
hydrate. The previously-orphan proxy.cache.toolkit.yaml is now
referenced by a test.
No peer-to-peer test pairs with this — k3po can't route a single
client connection to app0 against accepts at app1 and app2 without
the proxy in the middle, the same constraint that already excludes
tools.list.toolkit.multi from ApplicationIT.
https://github.com/aklivity/zilla/pull/1774
* test(binding-mcp): pair-up cache.hydrate.toolkit peer-to-peer
Add client.rpt for cache.hydrate.toolkit and the corresponding
shouldHydrateToolkit test in ProxyCacheLifecycleIT. The client
connects directly to app1 and app2 (rather than via app0),
sequenced with read notify / connect await barriers so the
script structurally mirrors the server.rpt's accepts at app1
and app2 — independent of any proxy in the middle.
https://github.com/aklivity/zilla/pull/1774
* refactor(binding-mcp): consolidate cache state in McpCacheContext
Merge McpLifecycleCache and McpListCache into a single per-binding
McpCacheContext that owns the store handle, guard, credentials, lease
ttl/retry, cache ttl, and mutable session/authorization state. The
former McpListCache is now an inner class of McpCacheContext exposed
per-kind via tools(), resources(), prompts() accessors.
Inline McpProxyCacheListHydrater plus its three kind-specific
subclasses as inner classes of McpProxyCacheHydrater so they share the
outer flyweights and reach session/authorization directly through the
enclosing context, dropping the LongSupplier / Supplier<String>
indirections.
Positions the hydrater for a future per-worker shared instance that
attaches/detaches contexts.
* refactor(binding-mcp): share McpProxyCacheHydrater across bindings
McpProxyCacheHydrater becomes a per-worker singleton owned by
McpProxyFactory, with attach(McpCacheContext) and detach(McpCacheContext)
methods that bind/unbind per-binding state. The three per-kind hydraters
collapse to three stateless strategy fields on the outer; all
state-machine methods take an McpCacheContext parameter and thread it
through.
McpCacheContext moves into internal.stream and absorbs the session
state (awaiters, populated, expected, complete, sessionId,
authorization) plus the bare-method-ref signal targets
(onInitiateLifecycle, onRefresh, register). Active stream handlers
register opaque Runnable cleanup hooks so detach can end them
without the context importing inner stream types.
McpBindingConfig drops the per-binding hydrater field; the lifecycle
factory now registers awaiters via binding.cacheContext.register.
* refactor(binding-mcp): drop unused McpCacheContext.listCache(kind)
The kind-keyed dispatcher is unreachable now that per-kind strategies
call tools() / resources() / prompts() directly via cacheOf(context).
* refactor(binding-mcp): drop unused per-kind cleanup hooks
Only the lifecycle stream needs explicit ending on detach — it is the
long-lived "I'm subscribed" stream with no self-termination trigger.
List streams self-terminate within a single round trip (closingInitial
is set in doListHydrateBegin so we end as soon as the server's WINDOW
arrives), and late callbacks on a detached context are already inert
because scheduleRefresh's onRefresh signal checks detached, markReady
bumps a counter against an empty awaiters list, and cache release is
idempotent.
Remove toolsCleanup / resourcesCleanup / promptsCleanup slots from
McpCacheContext, the setCleanup / clearCleanup abstract methods on
McpListHydrater with their three subclass overrides, and the
doListHydrateCleanup helper on McpListHydrateStream.
* refactor(binding-mcp): drop hydrater reference from cache context
McpCacheContext no longer holds back-reference to McpProxyCacheHydrater.
Signal scheduling sites use small context-capturing lambdas to invoke
hydrater methods directly (sigId -> beginLifecycle(context) and
sigId -> refresh(context)), so the trampoline methods onInitiateLifecycle
and onRefresh on the context are gone too.
McpCacheContext now takes the engine Signaler at construction time
(provided via McpBindingConfig from EngineContext) which it uses
directly for handle dispatch in register() and markComplete(). The
detached guard moves into beginLifecycle and refresh on the hydrater
where the orchestration actually happens.
bind(hydrater, expected) becomes prepare(expected) — just session
state reset, no hydrater stored.
The lambda allocation per signal scheduling fires at most a handful of
times per binding lifetime (attach, lease retry, cache TTL refresh) —
nothing on the hot path.
* refactor(binding-mcp): move orchestration into cache context
McpCacheContext takes the per-worker hydrater as a final ctor field
and owns the full lifecycle state machine. McpProxyFactory.attach
calls context.start() (no args) and context.detach() directly; the
hydrater's attach/detach/beg…
1 parent 115457a commit 1eb6b70
107 files changed
Lines changed: 11745 additions & 3019 deletions
File tree
- runtime
- binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream
- binding-mcp/src
- main/java/io/aklivity/zilla/runtime/binding/mcp
- config
- internal
- config
- stream
- cache
- test/java/io/aklivity/zilla/runtime/binding/mcp/internal
- stream
- binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench
- engine/src
- main/java/io/aklivity/zilla/runtime/engine
- concurrent
- internal/registry
- test
- java/io/aklivity/zilla/runtime/engine/test/internal
- resolver
- store
- config
- resources/META-INF/services
- specs
- binding-mcp.spec/src
- main/scripts/io/aklivity/zilla/specs/binding/mcp
- config
- schema
- streams/application
- cache.hydrate.100k
- cache.hydrate.10k
- cache.hydrate.credentials
- cache.hydrate.error
- cache.hydrate.lifecycle.reconnect
- cache.hydrate.toolkit
- cache.hydrate
- cache.refresh.prompts
- cache.refresh.resources
- cache.refresh.tools.contended
- cache.refresh.tools.error.retry
- cache.refresh.tools.error
- cache.refresh.tools
- cache.serve.initialize
- cache.serve.prompts.list
- cache.serve.resources.list
- cache.serve.tools.list.100k
- cache.serve.tools.list.10k
- cache.serve.tools.list.during.hydrate
- cache.serve.tools.list
- lifecycle.client.read.abort
- lifecycle.client.write.abort
- lifecycle.client.write.close
- lifecycle.server.read.abort
- lifecycle.server.write.abort
- lifecycle.server.write.close
- test/java/io/aklivity/zilla/specs/binding/mcp/streams
- application
- cache
- engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/schema/store
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 23 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
591 | 592 | | |
592 | 593 | | |
593 | 594 | | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
594 | 604 | | |
595 | 605 | | |
596 | 606 | | |
| |||
639 | 649 | | |
640 | 650 | | |
641 | 651 | | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
642 | 665 | | |
643 | 666 | | |
644 | 667 | | |
| |||
Lines changed: 4 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
39 | | - | |
| 40 | + | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
| 44 | + | |
42 | 45 | | |
43 | 46 | | |
Lines changed: 9 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
47 | 55 | | |
48 | 56 | | |
49 | 57 | | |
50 | | - | |
| 58 | + | |
51 | 59 | | |
52 | 60 | | |
runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfig.java
Lines changed: 48 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
Lines changed: 74 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | | - | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
31 | | - | |
| 32 | + | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| 38 | + | |
36 | 39 | | |
37 | 40 | | |
38 | 41 | | |
| |||
Lines changed: 14 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
74 | 87 | | |
75 | 88 | | |
76 | 89 | | |
| |||
81 | 94 | | |
82 | 95 | | |
83 | 96 | | |
84 | | - | |
| 97 | + | |
85 | 98 | | |
86 | 99 | | |
Lines changed: 60 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
23 | 26 | | |
| 27 | + | |
24 | 28 | | |
| 29 | + | |
25 | 30 | | |
| 31 | + | |
26 | 32 | | |
27 | 33 | | |
28 | 34 | | |
| |||
46 | 52 | | |
47 | 53 | | |
48 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
49 | 58 | | |
50 | 59 | | |
51 | 60 | | |
| |||
72 | 81 | | |
73 | 82 | | |
74 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
75 | 90 | | |
76 | 91 | | |
77 | 92 | | |
| |||
146 | 161 | | |
147 | 162 | | |
148 | 163 | | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
149 | 179 | | |
150 | 180 | | |
151 | 181 | | |
| |||
258 | 288 | | |
259 | 289 | | |
260 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
261 | 321 | | |
262 | 322 | | |
263 | 323 | | |
| |||
0 commit comments