diff --git a/README.md b/README.md index bd1af6c..7a46b3b 100644 --- a/README.md +++ b/README.md @@ -1190,7 +1190,7 @@ Methods: - `system.capabilities` returns gateway protocol and node policy snapshot. Companion runtime helper: -- `src/companion/runtimeClient.ts` provides a typed Node/WebSocket client for companion runtimes (macOS/iOS/Android workers) with wrappers for `node.register`, `node.capabilities.get`, `node.location.set/get`, `node.status.set`, `node.push_token.set`, `system.capabilities`, `system.nodes`, and canvas artifact RPCs (`canvas.put/get/list/delete/clear`), plus convenience helpers (`bootstrapNode`, optional `autoConnect`, `dispose()`) and event helpers (`subscribeEvents()`, `subscribeEvent()`, `subscribeAgentStream()`, `subscribeAgentTyping()`, `waitForEvent()` with timeout/predicate/abort support, `waitForAgentStream()`, `waitForAgentTyping()`, `clearEventSubscriptions()`). +- `src/companion/runtimeClient.ts` provides a typed Node/WebSocket client for companion runtimes (macOS/iOS/Android workers) with wrappers for `node.register`, `node.capabilities.get`, `node.location.set/get`, `node.status.set`, `node.push_token.set`, `system.capabilities`, `system.nodes`, and canvas artifact RPCs (`canvas.put/get/list/delete/clear`), plus convenience helpers (`bootstrapNode`, optional `autoConnect`, `dispose()`) and event helpers (`subscribeEvents()`, `subscribeEvent()`, `subscribeAgentStream()`, `subscribeAgentTyping()`, `subscribeContextWarning()`, `waitForEvent()` with timeout/predicate/abort support, `waitForAgentStream()`, `waitForAgentTyping()`, `waitForContextWarning()`, `clearEventSubscriptions()`). - `src/companion/platformClients.ts` provides platform-focused wrappers: - `MacOSCompanionClient` (`platform: "macos"`, APNs push registration) - `IOSCompanionClient` (`platform: "ios"`, APNs push registration) diff --git a/docs/plans/state.json b/docs/plans/state.json index d40ab8a..5c679d6 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -527,6 +527,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-runtime-context-warning-helpers": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Added dedicated context-warning event helpers on `CompanionRuntimeClient` (`subscribeContextWarning`, `waitForContextWarning`) for proactive context-pressure handling flows.", + "files_modified": [ + "src/companion/runtimeClient.ts", + "src/companion/runtimeClient.test.ts", + "README.md", + "docs/plans/state.json" + ], + "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" + }, "browser-tools-activation-clarity": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/runtimeClient.test.ts b/src/companion/runtimeClient.test.ts index 913634b..bc9a896 100644 --- a/src/companion/runtimeClient.test.ts +++ b/src/companion/runtimeClient.test.ts @@ -224,6 +224,24 @@ describe('CompanionRuntimeClient', () => { expect(typingHandler).toHaveBeenCalledWith({ active: true }); }); + it('supports subscribeContextWarning helper', () => { + const client = new CompanionRuntimeClient({ + url: 'ws://127.0.0.1:1', + }); + const warningHandler = vi.fn(); + client.subscribeContextWarning(warningHandler); + + (client as unknown as { handleMessage: (raw: string) => void }).handleMessage( + JSON.stringify({ + id: 56, + event: 'context_warning', + data: { thresholdPct: 80, estimatedPct: 92 }, + }), + ); + + expect(warningHandler).toHaveBeenCalledWith({ thresholdPct: 80, estimatedPct: 92 }); + }); + it('clears all event subscriptions', () => { const client = new CompanionRuntimeClient({ url: 'ws://127.0.0.1:1', @@ -355,6 +373,25 @@ describe('CompanionRuntimeClient', () => { await expect(awaited).resolves.toEqual({ active: true }); }); + it('waitForContextWarning resolves on context_warning events', async () => { + const client = new CompanionRuntimeClient({ + url: 'ws://127.0.0.1:1', + }); + + const awaited = client.waitForContextWarning<{ thresholdPct: number; estimatedPct: number }>({ + timeoutMs: 2000, + }); + (client as unknown as { handleMessage: (raw: string) => void }).handleMessage( + JSON.stringify({ + id: 57, + event: 'context_warning', + data: { thresholdPct: 75, estimatedPct: 88 }, + }), + ); + + await expect(awaited).resolves.toEqual({ thresholdPct: 75, estimatedPct: 88 }); + }); + it('connects and performs node registration + capability discovery', async () => { if (!LISTEN_ALLOWED) { return; diff --git a/src/companion/runtimeClient.ts b/src/companion/runtimeClient.ts index f2ac54d..119f24b 100644 --- a/src/companion/runtimeClient.ts +++ b/src/companion/runtimeClient.ts @@ -403,6 +403,12 @@ export class CompanionRuntimeClient { return this.subscribeEvent(COMPANION_EVENT_NAMES.agentTyping, handler); } + subscribeContextWarning( + handler: CompanionTypedEventHandler, + ): () => void { + return this.subscribeEvent(COMPANION_EVENT_NAMES.contextWarning, handler); + } + waitForEvent( eventName: string, options?: { @@ -475,6 +481,14 @@ export class CompanionRuntimeClient { return this.waitForEvent(COMPANION_EVENT_NAMES.agentTyping, options); } + waitForContextWarning(options?: { + timeoutMs?: number; + predicate?: CompanionEventPredicate; + signal?: AbortSignal; + }): Promise { + return this.waitForEvent(COMPANION_EVENT_NAMES.contextWarning, options); + } + async call(method: string, params?: Record): Promise { if (!this.connected) { if (!this.autoConnect) {