@theokit/sdk 3.2.2 → 3.2.3

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
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 1770aec: Fix (#59) — a stdio MCP client no longer permanently wedges after a transient outage exceeds the reconnect attempt bound. The bound is now LOCAL to each reconnect cycle (a bounded retry loop with backoff), so a later request re-arms a fresh cycle and reconnects once the server recovers — while a genuinely-broken server still surfaces a typed `mcp_disconnected` "reconnect exhausted". Adds the previously-missing HTTP-transport recovery test (stateless reconnect on the next request after a transport failure).
8
+ - cda0542: Fix (#60) — `Retry-After` now also parses the RFC-7231 HTTP-date form (`Retry-After: Wed, 21 Oct 2025 07:28:00 GMT`), converting it to seconds-until-then (clamped at 0 for a past date). Previously only the numeric-seconds form was honored; a date-form header was silently dropped. Clarified that the same-key 429 retry deliberately does not block on `Retry-After` (a multi-key pool rotates to a fresh key immediately; the cooldown is honored at pool-selection level).
9
+ - 510041b: Fix (#61) — the Anthropic streaming client now detects a truncated stream. A stream that closes cleanly-but-early (server FIN / proxy hiccup before the terminal `message_delta` carrying `stop_reason`) previously committed silently as a clean `end_turn`; it now throws a typed `NetworkError{code:"stream_truncated"}`, matching the OpenAI client's guard.
10
+ - f7d39c8: Fix (#63) — pagination now fails fast on invalid cursors. `paginate({ offset, limit })` rejected a `NaN` offset by silently returning the WHOLE list (and negative by returning empty); it now throws `ConfigurationError{code:"pagination_invalid"}` for any non-negative-integer offset/limit. Also adds real cross-process evidence for the conversation-storage file lock: two separate OS processes taking `withFileLock` on the same file are proven to serialize (previously only in-process concurrency was tested).
11
+
3
12
  ## 3.2.2
4
13
 
5
14
  ### Patch Changes
@@ -5159,12 +5159,23 @@ var init_objective_coerce = __esm({
5159
5159
  // src/internal/persistence/pagination.ts
5160
5160
  function paginate(items, opts) {
5161
5161
  if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
5162
- const start = Math.max(0, opts.offset ?? 0);
5163
- const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
5162
+ const start = opts.offset === void 0 ? 0 : requireNonNegativeInt(opts.offset, "offset");
5163
+ const limit = opts.limit === void 0 ? void 0 : requireNonNegativeInt(opts.limit, "limit");
5164
+ const end = limit === void 0 ? items.length : start + limit;
5164
5165
  return items.slice(start, end);
5165
5166
  }
5167
+ function requireNonNegativeInt(value, field) {
5168
+ if (!Number.isInteger(value) || value < 0) {
5169
+ throw new ConfigurationError(
5170
+ `Invalid pagination ${field}: expected a non-negative integer, got ${value}`,
5171
+ { code: "pagination_invalid" }
5172
+ );
5173
+ }
5174
+ return value;
5175
+ }
5166
5176
  var init_pagination = __esm({
5167
5177
  "src/internal/persistence/pagination.ts"() {
5178
+ init_errors();
5168
5179
  }
5169
5180
  });
5170
5181
 
@@ -10165,6 +10176,8 @@ function parseRetryAfter(headers) {
10165
10176
  if (raw === null) return void 0;
10166
10177
  const n = Number(raw);
10167
10178
  if (Number.isFinite(n) && n >= 0) return Math.ceil(n);
10179
+ const dateMs = Date.parse(raw);
10180
+ if (Number.isFinite(dateMs)) return Math.max(0, Math.ceil((dateMs - Date.now()) / 1e3));
10168
10181
  return void 0;
10169
10182
  }
10170
10183
  function truncateRaw(body) {
@@ -10485,6 +10498,7 @@ function buildAnthropicBody(request) {
10485
10498
  var AnthropicClient, AnthropicStreamAccumulator;
10486
10499
  var init_anthropic3 = __esm({
10487
10500
  "src/internal/llm/anthropic.ts"() {
10501
+ init_errors();
10488
10502
  init_anthropic2();
10489
10503
  init_anthropic_shared();
10490
10504
  init_finish();
@@ -10538,12 +10552,23 @@ var init_anthropic3 = __esm({
10538
10552
  const events = accumulator.consume(parsed);
10539
10553
  for (const event of events) yield event;
10540
10554
  }
10555
+ if (!accumulator.finishReasonSeen) {
10556
+ throw new NetworkError("Anthropic SSE stream truncated (no stop_reason)", {
10557
+ code: "stream_truncated"
10558
+ });
10559
+ }
10541
10560
  return accumulator.finish();
10542
10561
  }
10543
10562
  };
10544
10563
  AnthropicStreamAccumulator = class {
10545
10564
  text = "";
10546
10565
  stopReason = "end_turn";
10566
+ /**
10567
+ * M2 #61 — whether a `message_delta` carrying a real `stop_reason` was seen.
10568
+ * A stream that closes before it is a truncation (server FIN / proxy hiccup),
10569
+ * not a clean `end_turn` — the caller throws `stream_truncated` on `false`.
10570
+ */
10571
+ sawStopReason = false;
10547
10572
  inputTokens;
10548
10573
  outputTokens;
10549
10574
  cacheReadTokens;
@@ -10590,6 +10615,9 @@ var init_anthropic3 = __esm({
10590
10615
  * spinning the SSE parser.
10591
10616
  */
10592
10617
  handleMessageDelta(md) {
10618
+ if (md.delta.stop_reason !== void 0 && md.delta.stop_reason !== null) {
10619
+ this.sawStopReason = true;
10620
+ }
10593
10621
  this.stopReason = mapAnthropicStopReason(md.delta.stop_reason);
10594
10622
  if (md.usage?.input_tokens !== void 0) this.inputTokens = md.usage.input_tokens;
10595
10623
  if (md.usage?.output_tokens !== void 0) this.outputTokens = md.usage.output_tokens;
@@ -10600,6 +10628,10 @@ var init_anthropic3 = __esm({
10600
10628
  this.cacheReadTokens = md.usage.cache_read_input_tokens;
10601
10629
  }
10602
10630
  }
10631
+ /** M2 #61 — whether the terminal `message_delta` (stop_reason) was seen. */
10632
+ get finishReasonSeen() {
10633
+ return this.sawStopReason;
10634
+ }
10603
10635
  finish() {
10604
10636
  const toolCalls = [];
10605
10637
  for (const [index, tool] of this.toolCalls.entries()) {
@@ -12833,7 +12865,6 @@ var init_client = __esm({
12833
12865
  // concurrent request so parallel tool dispatch after a drop awaits one handshake
12834
12866
  // instead of racing (or spuriously failing with mcp_not_init).
12835
12867
  dropped = false;
12836
- reconnectAttempts = 0;
12837
12868
  reconnectPromise;
12838
12869
  get timeoutMs() {
12839
12870
  return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
@@ -12886,15 +12917,22 @@ var init_client = __esm({
12886
12917
  return this.reconnectPromise;
12887
12918
  }
12888
12919
  async reconnect() {
12889
- if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
12890
- throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
12920
+ let lastErr;
12921
+ for (let attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt += 1) {
12922
+ await reconnectDelay(attempt);
12923
+ this.spawnChild();
12924
+ try {
12925
+ await super.initialize();
12926
+ this.dropped = false;
12927
+ return;
12928
+ } catch (err) {
12929
+ lastErr = err;
12930
+ }
12891
12931
  }
12892
- await reconnectDelay(this.reconnectAttempts);
12893
- this.reconnectAttempts += 1;
12894
- this.spawnChild();
12895
- await super.initialize();
12896
- this.dropped = false;
12897
- this.reconnectAttempts = 0;
12932
+ throw new NetworkError(`MCP ${this.name} reconnect exhausted`, {
12933
+ code: "mcp_disconnected",
12934
+ ...lastErr instanceof Error ? { cause: lastErr } : {}
12935
+ });
12898
12936
  }
12899
12937
  async close() {
12900
12938
  this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));