From 6a9796066b501452d43490488e6000ca2fd8a912 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 19:33:55 -0800 Subject: [PATCH] feat(companion): add platform event subscription count passthrough --- README.md | 2 +- docs/plans/state.json | 13 +++++++++++++ src/companion/platformClients.test.ts | 13 +++++++++++++ src/companion/platformClients.ts | 12 ++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 658607c..f61db79 100644 --- a/README.md +++ b/README.md @@ -1200,7 +1200,7 @@ Companion runtime helper: - `createHeartbeatLoop()` convenience helper that returns a bound `CompanionHeartbeatLoop` - optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call - `dispose()` lifecycle helper for unified runtime teardown - - stream passthrough helpers (`subscribeEvents`, `clearEventSubscriptions`, `listKnownEventNames`, `subscribeAgentStream/Typing/ContextWarning`, `waitForAnyEvent`, `waitForAgentStream/Typing/ContextWarning`) + - stream passthrough helpers (`subscribeEvents`, `clearEventSubscriptions`, `listKnownEventNames`, `eventSubscriptionCount`, `subscribeAgentStream/Typing/ContextWarning`, `waitForAnyEvent`, `waitForAgentStream/Typing/ContextWarning`) - `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, optional interval jitter (`jitterRatio`) to spread load, `tickNow()` for manual sends, success/error hooks, failure observability (`failureCount`, `lastFailure`, `getState()`), and optional auto-stop after repeated failures. ## Canvas / A2UI Foundation diff --git a/docs/plans/state.json b/docs/plans/state.json index c23aa0c..01a9916 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -673,6 +673,19 @@ ], "test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" }, + "companion-platform-event-subscription-count-passthrough": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Added `eventSubscriptionCount` passthrough on platform companion clients so wrappers expose active runtime event-listener cardinality for observability and debugging.", + "files_modified": [ + "src/companion/platformClients.ts", + "src/companion/platformClients.test.ts", + "README.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" + }, "browser-tools-activation-clarity": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/platformClients.test.ts b/src/companion/platformClients.test.ts index 5e67688..cdf989b 100644 --- a/src/companion/platformClients.test.ts +++ b/src/companion/platformClients.test.ts @@ -35,6 +35,7 @@ function createRuntimeMock(): { waitForAgentTyping: ReturnType; waitForContextWarning: ReturnType; waitForAnyEvent: ReturnType; + eventSubscriptionCount: number; } { const connect = vi.fn(async () => undefined); const disconnect = vi.fn(() => undefined); @@ -70,6 +71,7 @@ function createRuntimeMock(): { const waitForAgentTyping = vi.fn(async () => ({ active: true })); const waitForContextWarning = vi.fn(async () => ({ thresholdPct: 75, estimatedPct: 90 })); const waitForAnyEvent = vi.fn(async () => ({ event: 'agent.stream', data: { token: 'any' } })); + const eventSubscriptionCount = 3; const runtime = { connect, @@ -99,6 +101,9 @@ function createRuntimeMock(): { waitForAgentTyping, waitForContextWarning, waitForAnyEvent, + get eventSubscriptionCount() { + return eventSubscriptionCount; + }, } as unknown as CompanionRuntimeClient; return { @@ -130,6 +135,7 @@ function createRuntimeMock(): { waitForAgentTyping, waitForContextWarning, waitForAnyEvent, + eventSubscriptionCount, }; } @@ -244,6 +250,13 @@ describe('platform companion clients', () => { expect(events).toEqual(['agent.stream', 'agent.typing', 'context_warning']); }); + it('platform eventSubscriptionCount forwards runtime getter value', async () => { + const mock = createRuntimeMock(); + const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' }); + + expect(client.eventSubscriptionCount).toBe(mock.eventSubscriptionCount); + }); + it('macOS client forwards canvas methods to runtime client', async () => { const mock = createRuntimeMock(); const client = new MacOSCompanionClient({ runtime: mock.runtime, nodeId: 'mac-node' }); diff --git a/src/companion/platformClients.ts b/src/companion/platformClients.ts index 7a23d84..36f5c68 100644 --- a/src/companion/platformClients.ts +++ b/src/companion/platformClients.ts @@ -260,6 +260,10 @@ export class MacOSCompanionClient { return this.runtime.listKnownEventNames(); } + get eventSubscriptionCount(): number { + return this.runtime.eventSubscriptionCount; + } + waitForAnyEvent( eventNames: readonly string[], options?: { @@ -476,6 +480,10 @@ export class IOSCompanionClient { return this.runtime.listKnownEventNames(); } + get eventSubscriptionCount(): number { + return this.runtime.eventSubscriptionCount; + } + waitForAnyEvent( eventNames: readonly string[], options?: { @@ -690,6 +698,10 @@ export class AndroidCompanionClient { return this.runtime.listKnownEventNames(); } + get eventSubscriptionCount(): number { + return this.runtime.eventSubscriptionCount; + } + waitForAnyEvent( eventNames: readonly string[], options?: {