@pouchy_ai/companion-sdk 0.29.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,37 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.29.1] - 2026-07-12
16
+
17
+ ### Fixed
18
+
19
+ - **`CompanionError.retryAfter` no longer reports a spurious `0`.** A missing
20
+ `Retry-After` header was parsed as `Number(null) === 0`, so EVERY error
21
+ thrown from a POST without the header (a `400 invalid_request`, a
22
+ `404 session_not_found`, …) carried `retryAfter: 0` — telling backoff code
23
+ "retry immediately" on non-retryable errors. It is now `undefined` unless
24
+ the body's `retryAfterSec` or a real header names a value, as documented.
25
+ - **`sendText({ awaitReply: true, stream: false })` now cancels its POST.**
26
+ The buffered leg never passed the operation's `AbortSignal` to the network
27
+ call, so the promise rejected on abort/deadline but the billed turn kept
28
+ running server-side. It now aborts with the same linked controller the
29
+ streaming leg uses.
30
+ - **Abort-listener leak on a reused `AbortSignal` in `awaitReply`.** The
31
+ deadline's reject listener was never removed, so a host reusing one
32
+ long-lived signal across many `sendText({ awaitReply, signal })` calls
33
+ accumulated one orphan listener per call (memory growth + Node's
34
+ MaxListenersExceeded warning). Removed in the same `finally` as the rest.
35
+
36
+ ### Added
37
+
38
+ - **`signal` on `sendToolResult` and `startCall`** — the two request-performing
39
+ methods 0.29.0's "AbortSignal on every network method" pass missed
40
+ (backwards-compatible addition, per this package's patch policy).
41
+ - **`require()` of the package now works on Node ≥ 20.17** (require-ESM): the
42
+ `exports` map gained a `default` condition — previously `require()` failed
43
+ with `ERR_PACKAGE_PATH_NOT_EXPORTED` before the feature could apply, despite
44
+ the README documenting it.
45
+
15
46
  ## [0.29.0] - 2026-07-12
16
47
 
17
48
  ### Added
@@ -616,7 +647,8 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
616
647
  - **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
617
648
  server by `protocol.drift.test.ts` (fails CI on divergence).
618
649
 
619
- [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...HEAD
650
+ [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.1...HEAD
651
+ [0.29.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...companion-sdk-v0.29.1
620
652
  [0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
621
653
  [0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
622
654
  [0.28.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.27.0...companion-sdk-v0.28.0
package/README.md CHANGED
@@ -122,12 +122,14 @@ are offered automatically once you declare `tools`/`handles`, and
122
122
  embeds receive expression cues — declare them or they no-op silently. Full
123
123
  contract: `docs/companion-host-control.md`.
124
124
 
125
- **Cancellation (0.29.0):** every request-performing method accepts an optional
126
- `AbortSignal`. Methods with an options object take a `signal?` field
127
- (`sendText(text, { signal })`, `recall({ signal })`, …); the rest take a
128
- trailing `opts` (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting
129
- rejects with `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })`
130
- it also tears down the in-flight event stream.
125
+ **Cancellation (0.29.0, completed in 0.29.1):** every request-performing method
126
+ accepts an optional `AbortSignal` including `sendToolResult(callId, result,
127
+ { signal })` and `startCall({ signal })` since 0.29.1. Methods with an options
128
+ object take a `signal?` field (`sendText(text, { signal })`,
129
+ `recall({ signal })`, …); the rest take a trailing `opts`
130
+ (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting rejects with
131
+ `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })` it also
132
+ tears down the in-flight event stream.
131
133
 
132
134
  ```ts
133
135
  const ac = new AbortController();
package/dist/client.d.ts CHANGED
@@ -346,6 +346,7 @@ export declare class CompanionClient {
346
346
  startCall(opts?: {
347
347
  voice?: string;
348
348
  locale?: string;
349
+ signal?: AbortSignal;
349
350
  }): Promise<CallCredentials>;
350
351
  /** Open a live voice call end-to-end: mints credentials (startCall) and opens a
351
352
  * WebRTC session directly to the provider — no first-party app code needed.
@@ -388,6 +389,8 @@ export declare class CompanionClient {
388
389
  sendToolResult(callId: string, result: {
389
390
  ok?: boolean;
390
391
  result?: unknown;
392
+ }, opts?: {
393
+ signal?: AbortSignal;
391
394
  }): Promise<{
392
395
  allDone: boolean;
393
396
  }>;
package/dist/client.js CHANGED
@@ -191,7 +191,13 @@ export class CompanionClient {
191
191
  const fromBody = json?.retryAfterSec;
192
192
  if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
193
193
  return fromBody;
194
- const header = Number(res.headers.get('Retry-After'));
194
+ // Missing/blank header must stay undefined — Number(null) is 0, which
195
+ // used to stamp retryAfter: 0 on EVERY error without the header (a 400,
196
+ // a 404 …), telling backoff code "retry immediately".
197
+ const raw = res.headers.get('Retry-After');
198
+ if (raw === null || raw.trim() === '')
199
+ return undefined;
200
+ const header = Number(raw);
195
201
  return Number.isFinite(header) && header >= 0 ? header : undefined;
196
202
  }
197
203
  async postJson(path, body, signal) {
@@ -305,6 +311,9 @@ export class CompanionClient {
305
311
  opts.signal.addEventListener('abort', onCallerAbort, { once: true });
306
312
  }
307
313
  let timer;
314
+ // Held so the finally can REMOVE it — a host reusing one long-lived signal
315
+ // across many awaitReply calls accumulated one orphan listener per call.
316
+ let onCallerAbortReject;
308
317
  const deadline = new Promise((_, reject) => {
309
318
  timer = setTimeout(() => {
310
319
  ac.abort();
@@ -313,7 +322,8 @@ export class CompanionClient {
313
322
  // The caller's signal also rejects the operation (the stream teardown
314
323
  // alone would otherwise leave the fallback wait hanging to the timer).
315
324
  if (opts?.signal) {
316
- opts.signal.addEventListener('abort', () => reject(new CompanionError('request aborted', 0, 'aborted')), { once: true });
325
+ onCallerAbortReject = () => reject(new CompanionError('request aborted', 0, 'aborted'));
326
+ opts.signal.addEventListener('abort', onCallerAbortReject, { once: true });
317
327
  }
318
328
  });
319
329
  try {
@@ -322,7 +332,10 @@ export class CompanionClient {
322
332
  let pausedOnTools = false;
323
333
  if (opts?.stream === false) {
324
334
  const json = await Promise.race([
325
- this.postJson(`/api/companion/session/${sessionId}/input`, body),
335
+ // ac.signal, like the streaming leg: the deadline and the caller's
336
+ // signal both abort it — without this the promise rejected but the
337
+ // billed POST kept running beyond cancellation.
338
+ this.postJson(`/api/companion/session/${sessionId}/input`, body, ac.signal),
326
339
  deadline
327
340
  ]);
328
341
  seq = json.seq ?? null;
@@ -354,6 +367,8 @@ export class CompanionClient {
354
367
  clearTimeout(timer);
355
368
  if (onCallerAbort)
356
369
  opts?.signal?.removeEventListener('abort', onCallerAbort);
370
+ if (onCallerAbortReject)
371
+ opts?.signal?.removeEventListener('abort', onCallerAbortReject);
357
372
  }
358
373
  }
359
374
  /** The streaming leg of sendText: parse the POST response's SSE frames —
@@ -512,10 +527,7 @@ export class CompanionClient {
512
527
  */
513
528
  async startCall(opts) {
514
529
  const id = this.requireSession();
515
- return this.postJson(`/api/companion/session/${id}/call`, {
516
- voice: opts?.voice,
517
- locale: opts?.locale
518
- });
530
+ return this.postJson(`/api/companion/session/${id}/call`, { voice: opts?.voice, locale: opts?.locale }, opts?.signal);
519
531
  }
520
532
  /** Open a live voice call end-to-end: mints credentials (startCall) and opens a
521
533
  * WebRTC session directly to the provider — no first-party app code needed.
@@ -651,7 +663,7 @@ export class CompanionClient {
651
663
  /** Report the result of a companion.tool_call this surface performed. When all
652
664
  * of the turn's calls are reported, the companion resumes and the continuation
653
665
  * (a companion.message or more tool calls) arrives on the event stream. */
654
- async sendToolResult(callId, result) {
666
+ async sendToolResult(callId, result, opts) {
655
667
  // Voice tool-call: resolve it locally so the result flows back to the voice
656
668
  // provider, not to a server text turn. (Same call shape as the text path.)
657
669
  const pending = this.pendingVoiceTools.get(callId);
@@ -660,7 +672,7 @@ export class CompanionClient {
660
672
  return { allDone: true };
661
673
  }
662
674
  const id = this.requireSession();
663
- const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result });
675
+ const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result }, opts?.signal);
664
676
  return { allDone: json.allDone ?? false };
665
677
  }
666
678
  /** A tool the live voice call asked the app to run. Fires the SAME
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.29.0",
4
- "description": "Embed the Pouchy companion \u2014 chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging \u2014 in any app, game, or site.",
3
+ "version": "0.29.1",
4
+ "description": "Embed the Pouchy companion chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging in any app, game, or site.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://pouchy.ai",
@@ -16,7 +16,8 @@
16
16
  "exports": {
17
17
  ".": {
18
18
  "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js"
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
20
21
  }
21
22
  },
22
23
  "main": "./dist/index.js",