chainlesschain 0.166.132 → 0.166.135

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.
Files changed (32) hide show
  1. package/README.md +44 -18
  2. package/package.json +1 -1
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/data/changelog.json +26 -0
  6. package/src/gateways/ws/llm-chat-protocol.js +1 -1
  7. package/src/gateways/ws/personal-data-hub-protocol.js +27 -11
  8. package/src/gateways/ws/session-core-protocol.js +22 -17
  9. package/src/gateways/ws/session-protocol.js +19 -8
  10. package/src/gateways/ws/ws-agent-handler.js +30 -0
  11. package/src/lib/background-launch-profile.js +37 -1
  12. package/src/lib/chat-core.js +40 -35
  13. package/src/lib/coding-task-guidance.js +10 -9
  14. package/src/lib/context-memory-kernel/message-adapter.js +5 -1
  15. package/src/lib/evolution/agent-model-projection.js +0 -1
  16. package/src/lib/evolution/governed-hub-llm.js +5 -1
  17. package/src/lib/evolution/governed-hub-resolver.js +15 -6
  18. package/src/lib/evolution/governed-hub-skill.js +2 -0
  19. package/src/lib/evolution/optional-ui-model-turn.js +13 -2
  20. package/src/lib/llm-config-defaults.js +17 -5
  21. package/src/lib/llm-connection-config.js +10 -2
  22. package/src/lib/llm-connection-probe.js +5 -2
  23. package/src/lib/llm-connection-resolution.js +7 -4
  24. package/src/lib/llm-providers.js +24 -11
  25. package/src/lib/openai-responses.js +2 -1
  26. package/src/lib/personal-data-hub-wiring.js +69 -36
  27. package/src/lib/provider-protocol.js +35 -3
  28. package/src/lib/provider-stream.js +6 -4
  29. package/src/lib/runnable-provider.js +4 -0
  30. package/src/lib/ws-chat-handler.js +22 -9
  31. package/src/runtime/agent-core.js +182 -34
  32. package/src/runtime/headless-runner.js +3 -3
@@ -10,7 +10,10 @@ import {
10
10
  normalizeProviderBaseUrl,
11
11
  openAICompatibleBaseUrl,
12
12
  } from "../lib/provider-protocol.js";
13
- import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
13
+ import {
14
+ BUILT_IN_PROVIDERS,
15
+ resolveProviderDefinition,
16
+ } from "../lib/llm-providers.js";
14
17
  import { resolveLlmConnection } from "../lib/llm-connection-resolution.js";
15
18
  /**
16
19
  * Agent Core — transport-independent agentic logic
@@ -12122,11 +12125,65 @@ function getEffectiveToolDefinitions(options = {}) {
12122
12125
  });
12123
12126
  }
12124
12127
 
12128
+ function projectExactFileMutationToolSchemas(
12129
+ definitions,
12130
+ scope,
12131
+ externalToolExecutors,
12132
+ ) {
12133
+ if (scope == null) return definitions;
12134
+ return definitions.map((tool) => {
12135
+ const name = tool?.function?.name;
12136
+ if (
12137
+ !["write_file", "edit_file", "edit_file_hashed"].includes(name) ||
12138
+ !AGENT_TOOLS.includes(tool) ||
12139
+ externalToolExecutors?.[name]
12140
+ ) {
12141
+ return tool;
12142
+ }
12143
+ const parameters = tool.function.parameters;
12144
+ return {
12145
+ ...tool,
12146
+ function: {
12147
+ ...tool.function,
12148
+ parameters: {
12149
+ ...parameters,
12150
+ properties: {
12151
+ ...parameters.properties,
12152
+ path: {
12153
+ ...parameters.properties.path,
12154
+ // Complete host-authorized set: never truncate this enum or
12155
+ // derive it from a read result. Normal tool context budgeting
12156
+ // sees the same schema that is sent to the provider.
12157
+ enum: [...scope.allowedPaths],
12158
+ description:
12159
+ "Use exactly one listed canonical repo-relative path. Read tools may return absolute paths; those are not valid mutation arguments in this run. Do not use an absolute path, ./ prefix, .. segment or backslash. This schema is guidance; all execution-time permission and file-identity checks still apply.",
12160
+ },
12161
+ },
12162
+ },
12163
+ },
12164
+ };
12165
+ });
12166
+ }
12167
+
12125
12168
  function getProviderToolDefinitions(options = {}) {
12126
- return projectMcpToolDefinitions(
12169
+ const definitions = projectMcpToolDefinitions(
12127
12170
  getEffectiveToolDefinitions(options),
12128
12171
  options.externalToolExecutors,
12129
12172
  );
12173
+ if (options.fileMutationScope == null) return definitions;
12174
+ if (options.exactToolNames !== true || options.hermeticExecution !== true) {
12175
+ throw new Error(
12176
+ "exact file mutation scope requires exactToolNames=true and hermeticExecution=true",
12177
+ );
12178
+ }
12179
+ const scope = normalizeExactFileMutationScope(options.fileMutationScope, {
12180
+ cwd: options.cwd || process.cwd(),
12181
+ });
12182
+ return projectExactFileMutationToolSchemas(
12183
+ definitions,
12184
+ scope,
12185
+ options.externalToolExecutors,
12186
+ );
12130
12187
  }
12131
12188
 
12132
12189
  const PROVIDER_REQUEST_ID_RE = /^[\x21-\x7e]{1,512}$/;
@@ -12540,7 +12597,7 @@ export async function chatWithTools(rawMessages, options) {
12540
12597
  }
12541
12598
 
12542
12599
  // OpenAI-compatible providers
12543
- const providerDefinition = BUILT_IN_PROVIDERS[provider];
12600
+ const providerDefinition = resolveProviderDefinition(provider);
12544
12601
  const url = openAICompatibleBaseUrl(provider, baseUrl);
12545
12602
 
12546
12603
  if (!providerDefinition || !url) {
@@ -12555,7 +12612,8 @@ export async function chatWithTools(rawMessages, options) {
12555
12612
 
12556
12613
  const envKey = providerDefinition.apiKeyEnv;
12557
12614
  const key = apiKey;
12558
- if (!key) throw new Error(`${envKey} required for provider ${provider}`);
12615
+ if (!key && provider !== "custom")
12616
+ throw new Error(`${envKey} required for provider ${provider}`);
12559
12617
  const effectiveModel = model || providerDefinition.models?.[0];
12560
12618
  const modelProfile = resolveModelCapabilityProfile({
12561
12619
  provider,
@@ -12613,34 +12671,69 @@ export async function chatWithTools(rawMessages, options) {
12613
12671
  );
12614
12672
  }
12615
12673
 
12616
- const response = await providerFetch(`${url}/responses`, {
12617
- method: "POST",
12618
- headers: {
12619
- "Content-Type": "application/json",
12620
- Authorization: `Bearer ${key}`,
12621
- ...(supportsOpenAIRequestIdentity && providerRequestId
12622
- ? { "X-Client-Request-Id": providerRequestId }
12674
+ // A JSON-body disconnect has produced no accepted output or executable
12675
+ // tools. Apply the same bounded transport retry and usage settlement used
12676
+ // by streaming; HTTP/provider verdicts and governed workflow calls retain
12677
+ // their existing no-replay rules.
12678
+ return await _retryStreamingChat(
12679
+ (attempt = 0) =>
12680
+ _withResponsesRequestDeadline(
12681
+ async (requestSignal) => {
12682
+ const requestId = supportsOpenAIRequestIdentity
12683
+ ? _physicalProviderRequestId(providerRequestId, attempt)
12684
+ : null;
12685
+ const response = await providerFetch(`${url}/responses`, {
12686
+ method: "POST",
12687
+ headers: {
12688
+ "Content-Type": "application/json",
12689
+ Authorization: `Bearer ${key}`,
12690
+ ...(requestId ? { "X-Client-Request-Id": requestId } : {}),
12691
+ },
12692
+ signal: requestSignal,
12693
+ body: JSON.stringify(responseBody),
12694
+ });
12695
+ if (!response.ok) {
12696
+ throw await createProviderResponseError(provider, response);
12697
+ }
12698
+ const retryMetadata = captureStreamRetryMetadata(response);
12699
+ try {
12700
+ const data = await response.json();
12701
+ const out = normalizeOpenAIResponsesResponse(data);
12702
+ if (out.usage)
12703
+ out.usage = preserveRequestedServiceTierEvidence(
12704
+ out.usage,
12705
+ options.serviceTier,
12706
+ );
12707
+ delete out.responseId;
12708
+ const providerReceipt = supportsOpenAIRequestIdentity
12709
+ ? _openAIProviderRequestReceipt(response, data, requestId)
12710
+ : null;
12711
+ if (providerReceipt) out.providerReceipt = providerReceipt;
12712
+ return out;
12713
+ } catch (error) {
12714
+ attachStreamRetryMetadata(error, retryMetadata);
12715
+ throw error;
12716
+ }
12717
+ },
12718
+ signal,
12719
+ options.streamStallTimeoutMs,
12720
+ ),
12721
+ {
12722
+ signal,
12723
+ retries: options.workflowEffectId ? 0 : undefined,
12724
+ strictRetryObserver: options.strictUsageTelemetry === true,
12725
+ ...(typeof options.onStreamRetry === "function"
12726
+ ? {
12727
+ onRetry: (attempt, error, telemetry) =>
12728
+ options.onStreamRetry(attempt, error, {
12729
+ ...telemetry,
12730
+ provider,
12731
+ model: effectiveModel,
12732
+ }),
12733
+ }
12623
12734
  : {}),
12624
12735
  },
12625
- signal,
12626
- body: JSON.stringify(responseBody),
12627
- });
12628
- if (!response.ok) {
12629
- throw await createProviderResponseError(provider, response);
12630
- }
12631
- const data = await response.json();
12632
- const out = normalizeOpenAIResponsesResponse(data);
12633
- if (out.usage)
12634
- out.usage = preserveRequestedServiceTierEvidence(
12635
- out.usage,
12636
- options.serviceTier,
12637
- );
12638
- delete out.responseId;
12639
- const providerReceipt = supportsOpenAIRequestIdentity
12640
- ? _openAIProviderRequestReceipt(response, data, providerRequestId)
12641
- : null;
12642
- if (providerReceipt) out.providerReceipt = providerReceipt;
12643
- return out;
12736
+ );
12644
12737
  }
12645
12738
 
12646
12739
  // Real token streaming (--include-partial-messages) for every OpenAI-compatible
@@ -12695,7 +12788,7 @@ export async function chatWithTools(rawMessages, options) {
12695
12788
  method: "POST",
12696
12789
  headers: {
12697
12790
  "Content-Type": "application/json",
12698
- Authorization: `Bearer ${key}`,
12791
+ ...(key ? { Authorization: `Bearer ${key}` } : {}),
12699
12792
  ...(supportsOpenAIRequestIdentity && providerRequestId
12700
12793
  ? { "X-Client-Request-Id": providerRequestId }
12701
12794
  : {}),
@@ -12989,6 +13082,36 @@ const STREAM_STALL_MS = 20000;
12989
13082
  const STREAM_STALL_TIMEOUT_MS = 180000;
12990
13083
  const _STREAM_STALL = Symbol("stream-stall");
12991
13084
 
13085
+ /** Bound the whole JSON attempt, including headers and a stalled body read. */
13086
+ async function _withResponsesRequestDeadline(
13087
+ operation,
13088
+ signal,
13089
+ configuredTimeout,
13090
+ ) {
13091
+ const timeoutMs = configuredTimeout ?? STREAM_STALL_TIMEOUT_MS;
13092
+ if (timeoutMs === 0) return operation(signal);
13093
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2_147_483_647)
13094
+ throw new RangeError("Invalid Responses request timeout");
13095
+ const controller = new AbortController();
13096
+ const requestSignal = signal
13097
+ ? AbortSignal.any([signal, controller.signal])
13098
+ : controller.signal;
13099
+ const timer = setTimeout(() => {
13100
+ const error = new Error(
13101
+ "OpenAI Responses request timed out before its complete response was received",
13102
+ );
13103
+ error.code = "ETIMEDOUT";
13104
+ controller.abort(error);
13105
+ }, timeoutMs);
13106
+ timer.unref?.();
13107
+ try {
13108
+ throwIfAborted(requestSignal);
13109
+ return await raceWithAbort(operation(requestSignal), requestSignal);
13110
+ } finally {
13111
+ clearTimeout(timer);
13112
+ }
13113
+ }
13114
+
12992
13115
  /**
12993
13116
  * Run a streaming chat attempt with bounded auto-retry on transient API
12994
13117
  * connection drops (Claude-Code 2.1.181: "auto-retry for API connection drops
@@ -13118,7 +13241,9 @@ export async function* _iterateStreamWithStall(reader, opts = {}) {
13118
13241
  // socket) and throw a retryable error the dispatch seam re-issues.
13119
13242
  if (timeoutMs && elapsed >= timeoutMs) {
13120
13243
  try {
13121
- await reader.cancel();
13244
+ // Cancellation itself can hang on a broken transport. Observe its
13245
+ // rejection, but never let it postpone the authoritative timeout.
13246
+ void Promise.resolve(reader.cancel()).catch(() => {});
13122
13247
  } catch {
13123
13248
  /* best-effort release — the throw below is what matters */
13124
13249
  }
@@ -13315,7 +13440,7 @@ async function _chatOpenAIStreaming(
13315
13440
  method: "POST",
13316
13441
  headers: {
13317
13442
  "Content-Type": "application/json",
13318
- Authorization: `Bearer ${apiKey}`,
13443
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
13319
13444
  ...(providerRequestId
13320
13445
  ? { "X-Client-Request-Id": providerRequestId }
13321
13446
  : {}),
@@ -13416,7 +13541,7 @@ async function _chatOpenAIResponsesStreaming(
13416
13541
  const decoder = new TextDecoder();
13417
13542
  let buf = "";
13418
13543
  try {
13419
- for await (const value of _iterateStreamWithStall(reader, {
13544
+ responses: for await (const value of _iterateStreamWithStall(reader, {
13420
13545
  onStall,
13421
13546
  stallMs,
13422
13547
  stallTimeoutMs,
@@ -13427,6 +13552,22 @@ async function _chatOpenAIResponsesStreaming(
13427
13552
  for (const line of lines) {
13428
13553
  reduceOpenAIResponsesStreamLine(state, line, ui.onToken, ui.onThinking);
13429
13554
  await ui.settle();
13555
+ if (state.terminalError) throw state.terminalError;
13556
+ if (state.normalized) {
13557
+ // response.completed/incomplete is authoritative. Some gateways
13558
+ // leave the HTTP stream alive after it; socket EOF is not required.
13559
+ // Observe an already-queued transport error (especially cancellation)
13560
+ // without waiting for a pending trailing read to finish.
13561
+ await Promise.race([reader.read(), Promise.resolve()]);
13562
+ throwIfAborted(signal);
13563
+ try {
13564
+ void Promise.resolve(reader.cancel()).catch(() => {});
13565
+ } catch {
13566
+ // The terminal response is already complete; cleanup is best effort.
13567
+ }
13568
+ buf = "";
13569
+ break responses;
13570
+ }
13430
13571
  }
13431
13572
  }
13432
13573
  if (buf.trim()) {
@@ -13468,6 +13609,12 @@ async function _chatOpenAIResponsesStreaming(
13468
13609
  );
13469
13610
  if (providerReceipt) out.providerReceipt = providerReceipt;
13470
13611
  return out;
13612
+ } finally {
13613
+ try {
13614
+ reader.releaseLock?.();
13615
+ } catch {
13616
+ // A cancelled or still-settling reader can reject lock release.
13617
+ }
13471
13618
  }
13472
13619
  const out = finalizeOpenAIResponsesStream(state);
13473
13620
  if (out.usage)
@@ -15018,6 +15165,7 @@ export async function* agentLoop(messages, options) {
15018
15165
  let effectiveToolOptions = hermeticExecution
15019
15166
  ? {
15020
15167
  ...options,
15168
+ fileMutationScope,
15021
15169
  hostManagedToolPolicy: null,
15022
15170
  hostManagedToolPolicyAuthority: null,
15023
15171
  extraToolDefinitions: [],
@@ -467,15 +467,15 @@ export function resolveEnabledTools({ allowedTools, readOnly } = {}) {
467
467
  return names;
468
468
  }
469
469
 
470
- /** Normalize a comma/space separated CLI list into a string[] (or null). */
470
+ /** An absent CLI list is unrestricted; an explicit empty list permits none. */
471
471
  export function parseToolList(value) {
472
- if (!value) return null;
472
+ if (value === null || value === undefined) return null;
473
473
  if (Array.isArray(value)) return value.flatMap((v) => parseToolList(v) || []);
474
474
  const out = String(value)
475
475
  .split(/[,\s]+/)
476
476
  .map((s) => s.trim())
477
477
  .filter(Boolean);
478
- return out.length > 0 ? out : null;
478
+ return out;
479
479
  }
480
480
 
481
481
  /**