@theokit/sdk 3.2.2 → 3.3.0

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e81e994: SE1 — the permission model is now resolved PER RUN. `SendOptions.permissionMode` (per-send) and `AgentOptions.permissionMode` (creation-time default) thread a `PermissionMode` (`default | plan | acceptEdits | bypass`, with `bypassPermissions` as the Anthropic-exact alias of `bypass`) into a registered `PermissionPlugin`'s pre-tool gate, with documented precedence (send > create > plugin construction > `default`). Also: the `canUseTool` gate is now fail-CLOSED on any non-`allow` decision (was fail-open — a malformed/undefined return previously allowed); a `g`/`y`-flag `RegExp` arg matcher is reset before each test (deterministic authorization). Full `PermissionMode` + `canUseTool` surface documented in docs.md.
8
+
9
+ ## 3.2.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 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).
14
+ - 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).
15
+ - 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.
16
+ - 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).
17
+
3
18
  ## 3.2.2
4
19
 
5
20
  ### 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
 
@@ -8834,7 +8845,9 @@ async function vetoFromPluginPreHook(inputs, call, callId, events) {
8834
8845
  name: call.name,
8835
8846
  args: call.input,
8836
8847
  agentId: inputs.agentId,
8837
- runId: inputs.runId
8848
+ runId: inputs.runId,
8849
+ // SE1 — thread the run's permission mode so a PermissionPlugin gates per-run.
8850
+ ...inputs.permissionMode !== void 0 ? { permissionMode: inputs.permissionMode } : {}
8838
8851
  });
8839
8852
  if (pluginVeto === void 0) return void 0;
8840
8853
  emitRunEvent(inputs.runEventSink, {
@@ -10165,6 +10178,8 @@ function parseRetryAfter(headers) {
10165
10178
  if (raw === null) return void 0;
10166
10179
  const n = Number(raw);
10167
10180
  if (Number.isFinite(n) && n >= 0) return Math.ceil(n);
10181
+ const dateMs = Date.parse(raw);
10182
+ if (Number.isFinite(dateMs)) return Math.max(0, Math.ceil((dateMs - Date.now()) / 1e3));
10168
10183
  return void 0;
10169
10184
  }
10170
10185
  function truncateRaw(body) {
@@ -10485,6 +10500,7 @@ function buildAnthropicBody(request) {
10485
10500
  var AnthropicClient, AnthropicStreamAccumulator;
10486
10501
  var init_anthropic3 = __esm({
10487
10502
  "src/internal/llm/anthropic.ts"() {
10503
+ init_errors();
10488
10504
  init_anthropic2();
10489
10505
  init_anthropic_shared();
10490
10506
  init_finish();
@@ -10538,12 +10554,23 @@ var init_anthropic3 = __esm({
10538
10554
  const events = accumulator.consume(parsed);
10539
10555
  for (const event of events) yield event;
10540
10556
  }
10557
+ if (!accumulator.finishReasonSeen) {
10558
+ throw new NetworkError("Anthropic SSE stream truncated (no stop_reason)", {
10559
+ code: "stream_truncated"
10560
+ });
10561
+ }
10541
10562
  return accumulator.finish();
10542
10563
  }
10543
10564
  };
10544
10565
  AnthropicStreamAccumulator = class {
10545
10566
  text = "";
10546
10567
  stopReason = "end_turn";
10568
+ /**
10569
+ * M2 #61 — whether a `message_delta` carrying a real `stop_reason` was seen.
10570
+ * A stream that closes before it is a truncation (server FIN / proxy hiccup),
10571
+ * not a clean `end_turn` — the caller throws `stream_truncated` on `false`.
10572
+ */
10573
+ sawStopReason = false;
10547
10574
  inputTokens;
10548
10575
  outputTokens;
10549
10576
  cacheReadTokens;
@@ -10590,6 +10617,9 @@ var init_anthropic3 = __esm({
10590
10617
  * spinning the SSE parser.
10591
10618
  */
10592
10619
  handleMessageDelta(md) {
10620
+ if (md.delta.stop_reason !== void 0 && md.delta.stop_reason !== null) {
10621
+ this.sawStopReason = true;
10622
+ }
10593
10623
  this.stopReason = mapAnthropicStopReason(md.delta.stop_reason);
10594
10624
  if (md.usage?.input_tokens !== void 0) this.inputTokens = md.usage.input_tokens;
10595
10625
  if (md.usage?.output_tokens !== void 0) this.outputTokens = md.usage.output_tokens;
@@ -10600,6 +10630,10 @@ var init_anthropic3 = __esm({
10600
10630
  this.cacheReadTokens = md.usage.cache_read_input_tokens;
10601
10631
  }
10602
10632
  }
10633
+ /** M2 #61 — whether the terminal `message_delta` (stop_reason) was seen. */
10634
+ get finishReasonSeen() {
10635
+ return this.sawStopReason;
10636
+ }
10603
10637
  finish() {
10604
10638
  const toolCalls = [];
10605
10639
  for (const [index, tool] of this.toolCalls.entries()) {
@@ -12833,7 +12867,6 @@ var init_client = __esm({
12833
12867
  // concurrent request so parallel tool dispatch after a drop awaits one handshake
12834
12868
  // instead of racing (or spuriously failing with mcp_not_init).
12835
12869
  dropped = false;
12836
- reconnectAttempts = 0;
12837
12870
  reconnectPromise;
12838
12871
  get timeoutMs() {
12839
12872
  return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
@@ -12886,15 +12919,22 @@ var init_client = __esm({
12886
12919
  return this.reconnectPromise;
12887
12920
  }
12888
12921
  async reconnect() {
12889
- if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
12890
- throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
12922
+ let lastErr;
12923
+ for (let attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt += 1) {
12924
+ await reconnectDelay(attempt);
12925
+ this.spawnChild();
12926
+ try {
12927
+ await super.initialize();
12928
+ this.dropped = false;
12929
+ return;
12930
+ } catch (err) {
12931
+ lastErr = err;
12932
+ }
12891
12933
  }
12892
- await reconnectDelay(this.reconnectAttempts);
12893
- this.reconnectAttempts += 1;
12894
- this.spawnChild();
12895
- await super.initialize();
12896
- this.dropped = false;
12897
- this.reconnectAttempts = 0;
12934
+ throw new NetworkError(`MCP ${this.name} reconnect exhausted`, {
12935
+ code: "mcp_disconnected",
12936
+ ...lastErr instanceof Error ? { cause: lastErr } : {}
12937
+ });
12898
12938
  }
12899
12939
  async close() {
12900
12940
  this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
@@ -13218,6 +13258,10 @@ function buildLoopInputs(options, runId, userText) {
13218
13258
  ...options.onDelta !== void 0 ? { onDelta: options.onDelta } : {},
13219
13259
  ...options.sendOptions.toolChoice !== void 0 ? { toolChoice: options.sendOptions.toolChoice } : {},
13220
13260
  ...options.sendOptions.activeTools !== void 0 ? { activeTools: options.sendOptions.activeTools } : {},
13261
+ // SE1 — resolve the run's permission mode: per-send wins over creation-time.
13262
+ ...(options.sendOptions.permissionMode ?? options.agentOptions.permissionMode) !== void 0 ? {
13263
+ permissionMode: options.sendOptions.permissionMode ?? options.agentOptions.permissionMode
13264
+ } : {},
13221
13265
  ...options.priorMessages !== void 0 ? { priorMessages: options.priorMessages } : {},
13222
13266
  ...options.memoryTools !== void 0 && options.memoryTools.length > 0 ? { memoryTools: options.memoryTools } : {},
13223
13267
  ...buildCustomToolsInput(