@bike4mind/cli 0.20.2 → 0.21.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as VoyageAIEmbeddingModel, A as HTTPError, At as resolveHistoryFetchLimit, B as PermissionDeniedError, Bt as isNearLimit, Ct as isRetryableError, D as FIXED_TEMPERATURE_MODELS, Dt as isZodError, E as FIELD_GROUP_OF, Et as isUserInitiatedAbort, F as ModelBackend, Ft as usdToCredits, G as SpeechToTextModels, H as REASONING_SUPPORTED_MODELS, I as NO_TEMPERATURE_MODELS, It as usdToCreditsStochastic, J as TooManyRequestsError, K as SupportedFabFileMimeTypes, L as NotFoundError, Lt as withRetry, M as ImageModels, Mt as settingsMap, N as InternalServerError, Nt as toModelInfo, O as FORMAT_PROMPT_TEMPLATE, Ot as mapMimeTypeToArtifactType, P as MODEL_INFO_FIELD_GROUP_OF, Pt as toModelRecord, Q as VideoModels, R as OllamaEmbeddingModel, Rt as buildRateLimitLogEntry, S as CorruptedFileError, St as isRenderableModelType, Tt as isUnlimitedHistory, U as REFUSAL_FALLBACK_MODELS, V as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Vt as parseRateLimitHeaders, W as RESPONSES_API_TOOL_MODELS, X as UnprocessableEntityError, Y as UnauthorizedError, Z as VIDEO_SIZE_CONSTRAINTS, _ as BadRequestError, _t as isImageServeable, at as getMcpProviderMetadata, bt as isModelDeprecated, ct as isAudioMimeType, et as WORK_ITEM_STATUSES, ft as isFieldGroup, g as BFL_SAFETY_TOLERANCE, gt as isImageAttachment, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as isGeminiModelId, it as defaultEmbeddingModelForEnv, j as HttpStatus, jt as secureParameters, k as ForbiddenError, kt as obfuscateApiKey, lt as isChunkRebuildPending, m as ApiKeyType, mt as isGPTImageModel, n as logger, nt as calculateRetryDelay, ot as getQuestErrorCode, p as ARTIFACT_ATTRS_PATTERN, pt as isGPTImage2Model, q as TTS_MAX_INPUT_CHARS, rt as dayjsConfig_default, st as getRetryAfterMs, tt as applyModelPriceCatalog, ut as isConvergencePausedNote, v as BedrockEmbeddingModel, vt as isMediaModelType, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isSupportedFabFileMimeType, x as ChatModels, xt as isPlaceholderApiKey, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, yt as isModelAccessible, z as OpenAIEmbeddingModel, zt as extractSnippetMeta } from "./ConfigStore-cIyF7hDg.mjs";
2
+ import { $ as SupportedFabFileMimeTypes, $t as buildRateLimitLogEntry, A as HTTPError, At as isRenderableModelType, B as OPENAI_GPT_IMAGE_1_IMAGE_SIZES, Bt as resolveHistoryFetchLimit, Ct as isGeminiModelId, D as FIXED_TEMPERATURE_MODELS, Dt as isModelAccessible, E as FIELD_GROUP_OF, Et as isMediaModelType, F as MODEL_INFO_FIELD_GROUP_OF, Ft as isZodError, G as PermissionDeniedError, Gt as usdToCredits, H as OllamaEmbeddingModel, Ht as settingsMap, I as McpServerName, It as mapMimeTypeToArtifactType, J as REFUSAL_FALLBACK_MODELS, K as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Kt as usdToCreditsStochastic, L as ModelBackend, Lt as obfuscateApiKey, M as IMAGE_SIZE_CONSTRAINTS, Mt as isSupportedFabFileMimeType, N as ImageModels, Nt as isUnlimitedHistory, O as FORMAT_PROMPT_TEMPLATE, Ot as isModelDeprecated, P as InternalServerError, Pt as isUserInitiatedAbort, Q as SpeechToTextModels, R as NO_TEMPERATURE_MODELS, Rt as parseEmbeddingRateLimitHeaders, S as CorruptedFileError, St as isGPTImageModel, Tt as isImageServeable, U as OpenAIEmbeddingModel, Ut as toModelInfo, V as OPENAI_GPT_IMAGE_2_IMAGE_SIZES, Vt as secureParameters, Wt as toModelRecord, Y as RESPONSES_API_TOOL_MODELS, _ as BadRequestError, _t as isChunkRebuildPending, at as VideoModels, bt as isFieldGroup, ct as applyModelPriceCatalog, dt as defaultEmbeddingModelForEnv, en as extractSnippetMeta, et as TTS_MAX_INPUT_CHARS, ft as getMcpProviderMetadata, g as BFL_SAFETY_TOLERANCE, gt as isAudioMimeType, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as hasUsableLimits, it as VIDEO_SIZE_CONSTRAINTS, j as HttpStatus, jt as isRetryableError, k as ForbiddenError, kt as isPlaceholderApiKey, lt as calculateRetryDelay, m as ApiKeyType, mt as getRetryAfterMs, n as logger, nn as parseRateLimitHeaders, nt as UnauthorizedError, ot as VoyageAIEmbeddingModel, p as ARTIFACT_ATTRS_PATTERN, pt as getQuestErrorCode, q as REASONING_SUPPORTED_MODELS, qt as withRetry, rt as UnprocessableEntityError, st as WORK_ITEM_STATUSES, tn as isNearLimit, tt as TooManyRequestsError, ut as dayjsConfig_default, v as BedrockEmbeddingModel, vt as isChunkStalledFile, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isImageAttachment, x as ChatModels, xt as isGPTImage2Model, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, z as NotFoundError, zt as reservationOutputTokens } from "./ConfigStore-CoY0l0gr.mjs";
3
3
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
4
4
  import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
5
5
  import { execFile, execFileSync, spawn } from "child_process";
@@ -52,9 +52,9 @@ import mongoose from "mongoose";
52
52
  import { parse } from "shell-quote";
53
53
  import { homedir as homedir$1 } from "node:os";
54
54
  import { EventEmitter } from "events";
55
+ import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
55
56
  import { fileURLToPath } from "url";
56
57
  import { Anthropic, RateLimitError } from "@anthropic-ai/sdk";
57
- import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
58
58
  import { GoogleGenAI } from "@google/genai";
59
59
  import pick from "lodash/pick.js";
60
60
  import { Stream } from "openai/streaming";
@@ -66,6 +66,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
66
66
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
67
67
  import { getDomain } from "tldts";
68
68
  import * as dotenv from "dotenv";
69
+ import { createHash as createHash$1 } from "node:crypto";
69
70
  import invert from "lodash/invert.js";
70
71
  import * as util from "node:util";
71
72
  import * as zlib from "node:zlib";
@@ -1836,7 +1837,27 @@ const webFetchTool = {
1836
1837
  })
1837
1838
  };
1838
1839
  //#endregion
1839
- //#region ../../b4m-core/services/dist/websearch-DpUKKZyj.mjs
1840
+ //#region ../../b4m-core/services/dist/websearch-BLmQCbHG.mjs
1841
+ /**
1842
+ * The coarse recency bucket both providers speak, as the smallest one containing `recencyDays`.
1843
+ * Null when there is no constraint, or when the window is wider than the widest bucket - a
1844
+ * "within 10 years" filter is not a filter, and sending one would exclude undated pages for nothing.
1845
+ */
1846
+ function recencyBucket(recencyDays) {
1847
+ if (typeof recencyDays !== "number" || !Number.isFinite(recencyDays) || recencyDays <= 0) return null;
1848
+ if (recencyDays <= 1) return "day";
1849
+ if (recencyDays <= 7) return "week";
1850
+ if (recencyDays <= 31) return "month";
1851
+ if (recencyDays <= 366) return "year";
1852
+ return null;
1853
+ }
1854
+ /** SerpAPI spells the buckets `qdr:d|w|m|y` on the `tbs` parameter. */
1855
+ const SERPAPI_QDR = {
1856
+ day: "qdr:d",
1857
+ week: "qdr:w",
1858
+ month: "qdr:m",
1859
+ year: "qdr:y"
1860
+ };
1840
1861
  const DEFAULT_NUM_RESULTS = 3;
1841
1862
  const SEARCH_TIMEOUT_MS = 6e4;
1842
1863
  /**
@@ -1845,14 +1866,14 @@ const SEARCH_TIMEOUT_MS = 6e4;
1845
1866
  * on a non-OK response so the tool surfaces the failure. Exported (re-exported from index) for the
1846
1867
  * REST endpoint and existing tests.
1847
1868
  */
1848
- async function serpApiSearch(adapters, query, num_results) {
1869
+ async function serpApiSearch(adapters, query, num_results, options) {
1849
1870
  const apiKey = await (0, apiKeyService_exports.getSerperKey)(adapters);
1850
1871
  const url = new URL("https://serpapi.com/search");
1851
1872
  if (!apiKey) {
1852
1873
  Logger.globalInstance.error("❌ WebSearch Tool: No API key configured. Skipping search.");
1853
1874
  return { organic_results: [] };
1854
1875
  }
1855
- url.search = new URLSearchParams({
1876
+ const searchParams = new URLSearchParams({
1856
1877
  engine: "google",
1857
1878
  api_key: apiKey,
1858
1879
  q: query,
@@ -1861,7 +1882,10 @@ async function serpApiSearch(adapters, query, num_results) {
1861
1882
  gl: "us",
1862
1883
  hl: "en",
1863
1884
  num: (num_results || DEFAULT_NUM_RESULTS).toString()
1864
- }).toString();
1885
+ });
1886
+ const bucket = recencyBucket(options?.recencyDays);
1887
+ if (bucket) searchParams.set("tbs", SERPAPI_QDR[bucket]);
1888
+ url.search = searchParams.toString();
1865
1889
  const controller = new AbortController();
1866
1890
  const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
1867
1891
  let response;
@@ -1889,8 +1913,8 @@ async function serpApiSearch(adapters, query, num_results) {
1889
1913
  function createSerpApiProvider(adapters) {
1890
1914
  return {
1891
1915
  name: "serpapi",
1892
- async search(query, numResults) {
1893
- const data = await serpApiSearch(adapters, query, numResults);
1916
+ async search(query, numResults, options) {
1917
+ const data = await serpApiSearch(adapters, query, numResults, options);
1894
1918
  return (Array.isArray(data.organic_results) ? data.organic_results : []).filter((r) => !!r && typeof r.link === "string").map((r) => ({
1895
1919
  title: r.title ?? r.link,
1896
1920
  url: r.link,
@@ -1929,16 +1953,19 @@ function parseSearxngResults(data, numResults) {
1929
1953
  function createSearxngProvider(baseUrl) {
1930
1954
  return {
1931
1955
  name: "searxng",
1932
- async search(query, numResults) {
1956
+ async search(query, numResults, options) {
1933
1957
  const limit = numResults && numResults > 0 ? numResults : DEFAULT_NUM_RESULTS;
1934
1958
  const trimmed = baseUrl.replace(/\/+$/, "");
1935
1959
  const url = new URL(`${trimmed}/search`);
1936
- url.search = new URLSearchParams({
1960
+ const params = new URLSearchParams({
1937
1961
  q: query,
1938
1962
  format: "json",
1939
1963
  language: "en",
1940
1964
  safesearch: "1"
1941
- }).toString();
1965
+ });
1966
+ const bucket = recencyBucket(options?.recencyDays);
1967
+ if (bucket) params.set("time_range", bucket);
1968
+ url.search = params.toString();
1942
1969
  const controller = new AbortController();
1943
1970
  const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
1944
1971
  try {
@@ -2061,7 +2088,7 @@ const webSearchTool = {
2061
2088
  })
2062
2089
  };
2063
2090
  //#endregion
2064
- //#region ../../b4m-core/services/dist/toolGenerators-D3QFkvc-.mjs
2091
+ //#region ../../b4m-core/services/dist/toolGenerators-hk-Robqc.mjs
2065
2092
  const diceRoll = async (parameters) => {
2066
2093
  if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
2067
2094
  return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
@@ -2748,7 +2775,7 @@ const askUserQuestionTool = {
2748
2775
  * re-export them without pulling the full tool graph. `index.ts` re-exports them
2749
2776
  * so the server barrel's public API is unchanged.
2750
2777
  */
2751
- const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2778
+ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionPreauthorizedLakeIds, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2752
2779
  const context = {
2753
2780
  userId,
2754
2781
  user,
@@ -2772,6 +2799,7 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
2772
2799
  fullyInlinedAttachmentIds,
2773
2800
  suppressLakeArms,
2774
2801
  sessionRetrievalTags,
2802
+ sessionPreauthorizedLakeIds,
2775
2803
  codeMinifier,
2776
2804
  availableModels,
2777
2805
  onToolLlmUsage,
@@ -4242,7 +4270,7 @@ const latticeAddEntityTool = {
4242
4270
  };
4243
4271
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4244
4272
  const model = await context.db.latticeModels.findById(modelId);
4245
- if (model) {
4273
+ if (model && model.userId === context.userId) {
4246
4274
  const existingIndex = model.data.entities.findIndex((e) => e.id === entityId);
4247
4275
  if (existingIndex >= 0) model.data.entities[existingIndex] = entityData;
4248
4276
  else model.data.entities.push(entityData);
@@ -4252,7 +4280,23 @@ const latticeAddEntityTool = {
4252
4280
  updatedAt: /* @__PURE__ */ new Date()
4253
4281
  });
4254
4282
  context.logger.info(`[Lattice] Added entity ${entityId} to model ${modelId}`);
4255
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4283
+ } else if (model) {
4284
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4285
+ return JSON.stringify({
4286
+ success: false,
4287
+ action: "ADD_ENTITY",
4288
+ modelId,
4289
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4290
+ });
4291
+ } else {
4292
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4293
+ return JSON.stringify({
4294
+ success: false,
4295
+ action: "ADD_ENTITY",
4296
+ modelId,
4297
+ error: `Model ${modelId} not found`
4298
+ });
4299
+ }
4256
4300
  } catch (error) {
4257
4301
  context.logger.error(`[Lattice] Failed to persist entity to database:`, error);
4258
4302
  }
@@ -4370,7 +4414,7 @@ const latticeSetValueTool = {
4370
4414
  const entityId = entityName.toLowerCase().replace(/\s+/g, "_");
4371
4415
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4372
4416
  const model = await context.db.latticeModels.findById(modelId);
4373
- if (model) {
4417
+ if (model && model.userId === context.userId) {
4374
4418
  const entity = model.data.entities.find((e) => e.id === entityId || e.name === entityName);
4375
4419
  if (entity) {
4376
4420
  const attrIndex = entity.attributes.findIndex((a) => a.key === attributeKey);
@@ -4390,7 +4434,23 @@ const latticeSetValueTool = {
4390
4434
  });
4391
4435
  context.logger.info(`[Lattice] Set ${entityId}.${attributeKey} = ${value} in model ${modelId}`);
4392
4436
  } else context.logger.warn(`[Lattice] Entity ${entityName} not found in model ${modelId}`);
4393
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4437
+ } else if (model) {
4438
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4439
+ return JSON.stringify({
4440
+ success: false,
4441
+ action: "SET_VALUE",
4442
+ modelId,
4443
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4444
+ });
4445
+ } else {
4446
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4447
+ return JSON.stringify({
4448
+ success: false,
4449
+ action: "SET_VALUE",
4450
+ modelId,
4451
+ error: `Model ${modelId} not found`
4452
+ });
4453
+ }
4394
4454
  } catch (error) {
4395
4455
  context.logger.error(`[Lattice] Failed to persist value to database:`, error);
4396
4456
  }
@@ -4489,7 +4549,7 @@ const latticeCreateRuleTool = {
4489
4549
  let entityCreatedMessage = "";
4490
4550
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4491
4551
  const model = await context.db.latticeModels.findById(modelId);
4492
- if (model) {
4552
+ if (model && model.userId === context.userId) {
4493
4553
  if (!model.data.entities.some((e) => e.id === outputEntityId || e.name.toLowerCase() === parsedRule.outputEntity.toLowerCase()) && parsedRule.outputEntity !== "unknown") {
4494
4554
  const now = /* @__PURE__ */ new Date();
4495
4555
  const newEntity = {
@@ -4526,7 +4586,23 @@ const latticeCreateRuleTool = {
4526
4586
  updatedAt: /* @__PURE__ */ new Date()
4527
4587
  });
4528
4588
  context.logger.info(`[Lattice] Created rule ${ruleId} in model ${modelId}`);
4529
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4589
+ } else if (model) {
4590
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4591
+ return JSON.stringify({
4592
+ success: false,
4593
+ action: "CREATE_RULE",
4594
+ modelId,
4595
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4596
+ });
4597
+ } else {
4598
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4599
+ return JSON.stringify({
4600
+ success: false,
4601
+ action: "CREATE_RULE",
4602
+ modelId,
4603
+ error: `Model ${modelId} not found`
4604
+ });
4605
+ }
4530
4606
  } catch (error) {
4531
4607
  context.logger.error(`[Lattice] Failed to persist rule to database:`, error);
4532
4608
  }
@@ -6413,6 +6489,130 @@ var EmbeddingAuthError = class extends Error {
6413
6489
  this.provider = provider;
6414
6490
  }
6415
6491
  };
6492
+ /**
6493
+ * Passive reporting of embedding-provider rate-limit ceilings.
6494
+ *
6495
+ * A rate limit belongs to the provider organization behind the key, and every embedding response
6496
+ * already carries the ceiling in its headers, so reading them costs no extra request and no extra
6497
+ * tokens. Providers that do not report them (Bedrock, Ollama) simply never produce an observation.
6498
+ *
6499
+ * This module owns the "what did the provider say" half only. It has no opinion about data lakes
6500
+ * or about the throughput levers configured against these numbers; interpreting a ceiling against
6501
+ * a lever belongs to the layer that knows what the levers are.
6502
+ */
6503
+ /**
6504
+ * Remaining/limit ratio at or below which the provider counts as under pressure. A bulk re-index
6505
+ * draws its window down steadily, so this sits low enough that ordinary throughput does not trip
6506
+ * it and only genuine starvation does.
6507
+ */
6508
+ const PRESSURE_RATIO = .1;
6509
+ /**
6510
+ * Pressure lasts as long as the window does, and every call in that window reports it. Throttle to
6511
+ * one line per interval so a starved ingest leaves a readable trace instead of flooding the log.
6512
+ */
6513
+ const PRESSURE_LOG_INTERVAL_MS = 6e4;
6514
+ /**
6515
+ * Process-local, and deliberately so: a cold start re-reports what it measures rather than leaving
6516
+ * a gap shared storage would have to close. Keyed by provider+model+account, so the map is bounded
6517
+ * by the model list times the number of distinct credentials the process serves.
6518
+ */
6519
+ const stateByKey = /* @__PURE__ */ new Map();
6520
+ /**
6521
+ * A broken reporter is indistinguishable from a steady ceiling - both are silence - so the first
6522
+ * fault has to be loud. Subsequent ones drop to debug: whatever breaks here breaks on every
6523
+ * embedding call, and a bulk re-index would drown the log in it.
6524
+ */
6525
+ let hasReportedFailure = false;
6526
+ const keyFor = (provider, model, account) => `${provider}:${model}:${account}`;
6527
+ const ceilingChanged = (previous, next) => previous.limitTokens !== next.limitTokens || previous.limitRequests !== next.limitRequests;
6528
+ const describeCeiling = (snapshot) => `${snapshot.limitTokens ?? "unreported"} tokens/min, ${snapshot.limitRequests ?? "unreported"} requests/min`;
6529
+ /** Ratio of the window still available, or null when the provider did not report that dimension. */
6530
+ const remainingRatio = (remaining, limit) => {
6531
+ if (remaining === null || limit === null || limit <= 0) return null;
6532
+ return remaining / limit;
6533
+ };
6534
+ const pressuredDimensions = (snapshot) => {
6535
+ const tokens = remainingRatio(snapshot.remainingTokens, snapshot.limitTokens);
6536
+ const requests = remainingRatio(snapshot.remainingRequests, snapshot.limitRequests);
6537
+ const dimensions = [];
6538
+ if (tokens !== null && tokens <= PRESSURE_RATIO) dimensions.push("tokens");
6539
+ if (requests !== null && requests <= PRESSURE_RATIO) dimensions.push("requests");
6540
+ return dimensions;
6541
+ };
6542
+ /**
6543
+ * Read the rate-limit headers off an embedding response and report the ceiling when it is worth
6544
+ * reporting: the first sighting in this process, a change since the last sighting, or the window
6545
+ * running down. Returns the observation when the provider reported a usable ceiling, else null.
6546
+ *
6547
+ * `account` identifies the provider account the reading belongs to and is part of the memo key,
6548
+ * not just the log line. The credential is resolved per user - a stored personal key beats the
6549
+ * platform key in `getEffectiveLLMApiKeys` - so one process can see several accounts on the same
6550
+ * provider+model. Without the discriminator their readings would collapse into one entry that
6551
+ * flaps between unrelated ceilings and attributes each figure to whoever reads the log next. The
6552
+ * caller supplies it; it must never be key material.
6553
+ *
6554
+ * Never throws. This hangs off the hot path of every embedding call, and a reporting fault must
6555
+ * not be able to fail an embedding that otherwise succeeded.
6556
+ */
6557
+ function recordEmbeddingRateLimitHeaders(provider, model, account, headers, now = Date.now()) {
6558
+ try {
6559
+ const snapshot = parseEmbeddingRateLimitHeaders(headers);
6560
+ if (!hasUsableLimits(snapshot)) return null;
6561
+ const key = keyFor(provider, model, account);
6562
+ const previous = stateByKey.get(key);
6563
+ const observation = {
6564
+ provider,
6565
+ model,
6566
+ account,
6567
+ snapshot,
6568
+ observedAt: now
6569
+ };
6570
+ const subject = `${provider} ${model} (account ${account})`;
6571
+ if (!previous) Logger.globalInstance.info(`[embedding-limits] ${subject} ceiling measured: ${describeCeiling(snapshot)}`, {
6572
+ provider,
6573
+ model,
6574
+ account,
6575
+ limitTokens: snapshot.limitTokens,
6576
+ limitRequests: snapshot.limitRequests
6577
+ });
6578
+ else if (ceilingChanged(previous.last.snapshot, snapshot)) Logger.globalInstance.warn(`[embedding-limits] ${subject} ceiling CHANGED: was ${describeCeiling(previous.last.snapshot)}, now ${describeCeiling(snapshot)}. Reconcile any throughput lever governed by this account against the new figure.`, {
6579
+ provider,
6580
+ model,
6581
+ account,
6582
+ previousLimitTokens: previous.last.snapshot.limitTokens,
6583
+ previousLimitRequests: previous.last.snapshot.limitRequests,
6584
+ limitTokens: snapshot.limitTokens,
6585
+ limitRequests: snapshot.limitRequests
6586
+ });
6587
+ const pressured = pressuredDimensions(snapshot);
6588
+ const dueForPressureLog = previous?.lastPressureLogAt == null || now - previous.lastPressureLogAt >= PRESSURE_LOG_INTERVAL_MS;
6589
+ const logPressure = pressured.length > 0 && dueForPressureLog;
6590
+ if (logPressure) Logger.globalInstance.warn(`[embedding-limits] ${subject} is at or below ${PRESSURE_RATIO * 100}% of its ${pressured.join(" and ")} window`, {
6591
+ provider,
6592
+ model,
6593
+ account,
6594
+ remainingTokens: snapshot.remainingTokens,
6595
+ remainingRequests: snapshot.remainingRequests,
6596
+ limitTokens: snapshot.limitTokens,
6597
+ limitRequests: snapshot.limitRequests,
6598
+ resetTokensMs: snapshot.resetTokensMs,
6599
+ resetRequestsMs: snapshot.resetRequestsMs
6600
+ });
6601
+ stateByKey.set(key, {
6602
+ last: observation,
6603
+ lastPressureLogAt: logPressure ? now : previous?.lastPressureLogAt ?? null
6604
+ });
6605
+ return observation;
6606
+ } catch (error) {
6607
+ const message = `[embedding-limits] failed to record rate-limit headers: ${error}`;
6608
+ if (hasReportedFailure) Logger.globalInstance.debug(message);
6609
+ else {
6610
+ hasReportedFailure = true;
6611
+ Logger.globalInstance.warn(message);
6612
+ }
6613
+ return null;
6614
+ }
6615
+ }
6416
6616
  const OPENAI_EMBEDDING_MODEL_MAP = {
6417
6617
  [OpenAIEmbeddingModel.TEXT_EMBEDDING_3_SMALL]: {
6418
6618
  provider: "OpenAI",
@@ -6433,34 +6633,72 @@ const OPENAI_EMBEDDING_MODEL_MAP = {
6433
6633
  dimensions: [1536]
6434
6634
  }
6435
6635
  };
6436
- var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6636
+ /**
6637
+ * Non-reversible stand-in for a credential, for use where two accounts have to be told apart in a
6638
+ * log. Same construction as the API-key logging hash in the request middleware. Never emit the key.
6639
+ */
6640
+ const fingerprintCredential = (apiKey) => `key:${createHash("sha256").update(apiKey).digest("hex").slice(0, 16)}`;
6641
+ /**
6642
+ * Total by construction. The only caller runs inside processSingleBatch's classifying try, where a
6643
+ * throw would be misread as a provider error and re-issue the batch.
6644
+ */
6645
+ const headerOrNull = (httpResponse, name) => {
6646
+ try {
6647
+ return httpResponse.headers?.get(name) ?? null;
6648
+ } catch {
6649
+ return null;
6650
+ }
6651
+ };
6652
+ /**
6653
+ * The ceilings `generateEmbeddingBatch` splits on, at module scope and exported because a cost
6654
+ * PREFLIGHT has to model the same split before it spends (packages/scripts/retrieval/capturePlan.ts).
6655
+ * A second copy of these numbers in a script cannot track a provider change.
6656
+ */
6657
+ const OPENAI_MAX_INPUTS_PER_REQUEST = 2048;
6658
+ const OPENAI_MAX_TOKENS_PER_INPUT = 8192;
6659
+ /**
6660
+ * Effective token limit with a 10% safety buffer.
6661
+ * The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
6662
+ * but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
6663
+ * that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
6664
+ */
6665
+ const OPENAI_EFFECTIVE_TOKEN_LIMIT = Math.floor(27e4);
6666
+ var OpenAIEmbeddingService = class {
6437
6667
  client;
6438
6668
  model;
6439
- /** Hard limit imposed by OpenAI's embeddings API. */
6440
- static MAX_TOKENS_PER_REQUEST = 3e5;
6441
- /**
6442
- * Effective token limit with a 10% safety buffer.
6443
- * The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
6444
- * but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
6445
- * that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
6446
- */
6447
- static EFFECTIVE_TOKEN_LIMIT = Math.floor(OpenAIEmbeddingService.MAX_TOKENS_PER_REQUEST * .9);
6669
+ credentialFingerprint;
6448
6670
  constructor(apiKey, model = OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002) {
6449
6671
  this.client = new OpenAI({ apiKey });
6450
6672
  this.validateModel(model);
6451
6673
  this.model = model;
6674
+ this.credentialFingerprint = fingerprintCredential(apiKey);
6675
+ }
6676
+ /**
6677
+ * Report the provider ceiling carried on a response we already received. Covers ingest and
6678
+ * query alike: both reach OpenAI through this class, so neither needs its own sampling point.
6679
+ *
6680
+ * The ceiling belongs to the organization behind the key, and the key is resolved per user
6681
+ * (getEffectiveLLMApiKeys prefers a stored personal key over the platform one), so the reading
6682
+ * has to say whose it is. `openai-organization` is the provider's own answer to that; the
6683
+ * credential fingerprint covers the case where the response omits it, and still keeps two
6684
+ * distinct keys as two readings rather than one that flaps between them.
6685
+ */
6686
+ recordRateLimit(httpResponse) {
6687
+ const account = headerOrNull(httpResponse, "openai-organization") || this.credentialFingerprint;
6688
+ recordEmbeddingRateLimitHeaders("OpenAI", this.model, account, httpResponse.headers);
6452
6689
  }
6453
6690
  validateModel(model) {
6454
6691
  if (!OPENAI_EMBEDDING_MODEL_MAP[model]) throw new Error(`Invalid OpenAI embedding model: ${model}`);
6455
6692
  }
6456
6693
  async generateEmbedding(text) {
6457
- const response = await this.client.embeddings.create({
6694
+ const { data: body, response: httpResponse } = await this.client.embeddings.create({
6458
6695
  model: this.model,
6459
6696
  input: text
6460
- }).catch((error) => {
6697
+ }).withResponse().catch((error) => {
6461
6698
  throw this.toActionableAuthError(error);
6462
6699
  });
6463
- if (response.data && response.data.length > 0) return response.data[0].embedding;
6700
+ this.recordRateLimit(httpResponse);
6701
+ if (body.data && body.data.length > 0) return body.data[0].embedding;
6464
6702
  throw new Error("No embedding data received from OpenAI");
6465
6703
  }
6466
6704
  /**
@@ -6491,8 +6729,6 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6491
6729
  */
6492
6730
  async generateEmbeddingBatch(texts, tokenCounts) {
6493
6731
  if (texts.length === 0) return [];
6494
- const MAX_INPUTS_PER_REQUEST = 2048;
6495
- const MAX_TOKENS_PER_INPUT = 8192;
6496
6732
  let tokens;
6497
6733
  let needsRecalculation = false;
6498
6734
  if (!tokenCounts || tokenCounts.length !== texts.length) needsRecalculation = true;
@@ -6507,12 +6743,12 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6507
6743
  let totalTokens = 0;
6508
6744
  for (let i = 0; i < texts.length; i++) {
6509
6745
  const tokenCount = tokens[i];
6510
- if (tokenCount > MAX_TOKENS_PER_INPUT) throw new Error(`Input at index ${i} exceeds ${MAX_TOKENS_PER_INPUT} token limit (${tokenCount} tokens)`);
6746
+ if (tokenCount > 8192) throw new Error(`Input at index ${i} exceeds ${OPENAI_MAX_TOKENS_PER_INPUT} token limit (${tokenCount} tokens)`);
6511
6747
  totalTokens += tokenCount;
6512
6748
  }
6513
6749
  Logger.globalInstance.debug(`[OpenAI] Batch embedding: ${texts.length} inputs, ${totalTokens} total tokens`);
6514
- const batches = this.createBatches(texts, tokens, MAX_INPUTS_PER_REQUEST, OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT);
6515
- Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT} tokens)`);
6750
+ const batches = this.createBatches(texts, tokens, OPENAI_MAX_INPUTS_PER_REQUEST, OPENAI_EFFECTIVE_TOKEN_LIMIT);
6751
+ Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens)`);
6516
6752
  if (batches.length === 1) return await this.processSingleBatch(batches[0].texts);
6517
6753
  const allEmbeddings = new Array(texts.length);
6518
6754
  for (const batch of batches) (await this.processSingleBatch(batch.texts)).forEach((embedding, batchIndex) => {
@@ -6578,8 +6814,8 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6578
6814
  async processSingleBatch(texts, preCalculatedTokens) {
6579
6815
  const tokenCounts = preCalculatedTokens || await this.calculateTokenCounts(texts);
6580
6816
  const batchTokens = tokenCounts.reduce((sum, count) => sum + count, 0);
6581
- if (batchTokens > OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT) {
6582
- Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT} tokens), splitting recursively`);
6817
+ if (batchTokens > 27e4) {
6818
+ Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens), splitting recursively`);
6583
6819
  const mid = Math.ceil(texts.length / 2);
6584
6820
  const firstHalf = texts.slice(0, mid);
6585
6821
  const secondHalf = texts.slice(mid);
@@ -6588,13 +6824,14 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6588
6824
  const [firstEmbeddings, secondEmbeddings] = await Promise.all([this.processSingleBatch(firstHalf, firstTokens), this.processSingleBatch(secondHalf, secondTokens)]);
6589
6825
  return [...firstEmbeddings, ...secondEmbeddings];
6590
6826
  }
6591
- for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's 8192 token limit per input (${tokenCounts[i]} tokens). This indicates a data integrity issue - chunk should have been smaller. This chunk cannot be processed and the entire batch must fail.`);
6827
+ for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's ${OPENAI_MAX_TOKENS_PER_INPUT} token limit per input (${tokenCounts[i]} tokens). This indicates a data integrity issue - chunk should have been smaller. This chunk cannot be processed and the entire batch must fail.`);
6592
6828
  try {
6593
- const response = await this.client.embeddings.create({
6829
+ const { data: body, response: httpResponse } = await this.client.embeddings.create({
6594
6830
  model: this.model,
6595
6831
  input: texts
6596
- });
6597
- if (response.data && response.data.length > 0) return response.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
6832
+ }).withResponse();
6833
+ this.recordRateLimit(httpResponse);
6834
+ if (body.data && body.data.length > 0) return body.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
6598
6835
  else throw new Error("No embedding data received from OpenAI");
6599
6836
  } catch (error) {
6600
6837
  if (error instanceof OpenAI.AuthenticationError) throw this.toActionableAuthError(error);
@@ -8116,6 +8353,50 @@ async function fetchWithoutRedirects(url, timeoutMs) {
8116
8353
  validateStatus: (status) => status >= 200 && status < 300 || status >= 300 && status < 400
8117
8354
  });
8118
8355
  }
8356
+ const BLOCK_LEVEL_SELECTOR = `*:not(${"a, span, em, strong, b, i, u, code, kbd, samp, var, sub, sup, small, abbr, cite, q, time, mark, s, del, ins, bdi, bdo, wbr, ruby, rt, rp".split(", ").join("):not(")}):not(td):not(th)`;
8357
+ /**
8358
+ * Extract readable text from the WHOLE document, not just `<p>` elements. The single collector
8359
+ * this replaced was `<p>`-only and fell back to the raw HTML when it found none: on a page whose
8360
+ * content isn't inside `<p>` (an RFC page using `<pre>`) that meant the fallback fired and stored
8361
+ * markup verbatim; on a page with real substance in headings, list items, table cells or code
8362
+ * blocks alongside its `<p>`s, that content was silently dropped.
8363
+ *
8364
+ * `head` (title/meta/script/style all live there, and the caller already reads `<title>`
8365
+ * separately) plus any stray `script`/`style`/`noscript` outside it are removed before extraction,
8366
+ * so none of that reaches what gets embedded. `<pre>` content is pulled out and stashed BEFORE the
8367
+ * rest of the document is collapsed, and spliced back in verbatim afterward - it needs to skip the
8368
+ * whitespace-collapse below (a code block's leading-space indentation is meaningful, unlike prose
8369
+ * whitespace) but still needs to land in the right place relative to everything else. Table cells
8370
+ * get a trailing space (still the same row, but no longer jammed into the next cell's word); every
8371
+ * other block-level element gets a trailing newline; runs of whitespace and blank lines are then
8372
+ * collapsed. Returns `''` when nothing extractable was found, so the caller stores nothing rather
8373
+ * than falling back to raw HTML.
8374
+ */
8375
+ function extractReadableText($) {
8376
+ $("head, script, style, noscript").remove();
8377
+ $("br").replaceWith("\n");
8378
+ const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
8379
+ const markerFor = (index) => `\uE000PRE${nonce}_${index}\uE000`;
8380
+ const markerPattern = new RegExp(`\\uE000PRE${nonce}_(\\d+)\\uE000`, "g");
8381
+ const preBlocks = [];
8382
+ $("pre").each((_index, element) => {
8383
+ const text = $(element).text();
8384
+ if (text) {
8385
+ preBlocks.push(text);
8386
+ $(element).replaceWith(`${markerFor(preBlocks.length - 1)}\n`);
8387
+ } else $(element).remove();
8388
+ });
8389
+ $("td, th").each((_index, cell) => {
8390
+ $(cell).after(" ");
8391
+ });
8392
+ $(BLOCK_LEVEL_SELECTOR).each((_index, element) => {
8393
+ $(element).after("\n");
8394
+ });
8395
+ return $.root().text().split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter(Boolean).join("\n").replace(markerPattern, (match, indexStr) => {
8396
+ const index = Number(indexStr);
8397
+ return index >= 0 && index < preBlocks.length ? preBlocks[index] : match;
8398
+ });
8399
+ }
8119
8400
  async function fetchAndParseURL(url, { logger }) {
8120
8401
  logger.updateMetadata({ failedUrl: null });
8121
8402
  try {
@@ -8149,16 +8430,13 @@ async function fetchAndParseURL(url, { logger }) {
8149
8430
  const htmlContent = body.toString("utf8");
8150
8431
  const $ = cheerio.load(htmlContent);
8151
8432
  title = $("title").text() || lastPathSegment(currentUrl);
8152
- let textContent = "";
8153
- $("body").find("p").each((index, element) => {
8154
- textContent += $(element).text() + "\n";
8155
- });
8156
- urlContent = textContent || htmlContent;
8433
+ urlContent = extractReadableText($);
8157
8434
  }
8158
8435
  const original = redactUrlCredentials(url);
8159
8436
  const final = redactUrlCredentials(currentUrl);
8160
8437
  const fetched = original === final ? original : `${original} -> ${final}`;
8161
- logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
8438
+ if (urlContent === "") logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}, but no extractable text was found`);
8439
+ else logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
8162
8440
  return {
8163
8441
  title,
8164
8442
  textContent: urlContent,
@@ -12901,10 +13179,117 @@ const vm = require('node:vm');
12901
13179
  const STDOUT_HEAD_BYTES = ${5e3};
12902
13180
  const STDOUT_TAIL_BYTES = ${2e3};
12903
13181
  const HARD_PER_LINE_BYTES = ${5e4};
13182
+ const MIRROR_TAIL_FLUSH_MS = ${100};
12904
13183
 
12905
13184
  let stdoutChunks = [];
12906
- let stdoutBytes = 0;
12907
13185
  let truncated = false;
13186
+ // --- Mirror state --------------------------------------------------------
13187
+ // Mirrors this run's stdout to the main thread as it is produced, in the same
13188
+ // head + marker + tail shape collectStdout() produces, so a retired run and a
13189
+ // completed one report the same thing by the same rule.
13190
+ //
13191
+ // The two halves are cost-bounded differently. The head is mirrored line by
13192
+ // line, so a chatty loop stops paying per line once the head is full. Past
13193
+ // that the tail is kept locally in a rolling window and posted on a timer, so
13194
+ // the message rate stops tracking the line rate entirely.
13195
+ let currentRunId = null;
13196
+ let mirroredHeadBytes = 0;
13197
+ let headMirrorFull = false;
13198
+ let tailChunks = [];
13199
+ let tailBytes = 0;
13200
+ let elidedBytes = 0;
13201
+ let tailFlushTimer = null;
13202
+
13203
+ /**
13204
+ * What the rolling tail may hold: whatever the head did not use of the same
13205
+ * HEAD + TAIL total collectStdout() reports within. A fixed TAIL budget made
13206
+ * the two disagree whenever the head came up short - a single 6KB first line
13207
+ * does not fit the head, so the mirror would have kept 2KB of a run that
13208
+ * collectStdout() reports whole, and called it truncated. mirroredHeadBytes
13209
+ * is frozen once the head is full, so this is stable for the rest of the run.
13210
+ */
13211
+ function tailBudget() {
13212
+ return STDOUT_HEAD_BYTES + STDOUT_TAIL_BYTES - mirroredHeadBytes;
13213
+ }
13214
+
13215
+ function postToMain(msg) {
13216
+ try { parentPort.postMessage(msg); } catch { /* worker being torn down; nothing to preserve */ }
13217
+ }
13218
+ function cancelTailFlush() {
13219
+ if (tailFlushTimer === null) return;
13220
+ clearTimeout(tailFlushTimer);
13221
+ tailFlushTimer = null;
13222
+ }
13223
+ function flushTail() {
13224
+ if (currentRunId === null || !headMirrorFull) return;
13225
+ const joined = tailChunks.join('\n');
13226
+ // The rolling window is trimmed line by line, so it can only exceed the
13227
+ // budget by holding ONE line longer than the whole budget. Slice to the
13228
+ // same last-N-chars rule collectStdout() uses, which both matches that
13229
+ // path and keeps the flush payload bounded - a guest printing 50KB lines
13230
+ // would otherwise re-send 50KB on every tick.
13231
+ const overflow = Math.max(0, joined.length - tailBudget());
13232
+ postToMain({
13233
+ type: 'stdoutTail',
13234
+ id: currentRunId,
13235
+ tail: overflow > 0 ? joined.slice(overflow) : joined,
13236
+ // Counted from what was actually DROPPED - lines the rolling window
13237
+ // evicted, plus whatever this payload's own slice cuts - rather than
13238
+ // derived from the byte totals. The derived form read zero on the first
13239
+ // flush by construction (every line was still in the head, so the
13240
+ // subtraction cancelled), which made "truncated" unreportable on exactly
13241
+ // the run the mirror exists for.
13242
+ elidedBytes: elidedBytes + overflow,
13243
+ });
13244
+ }
13245
+ function scheduleTailFlush() {
13246
+ if (tailFlushTimer !== null) return;
13247
+ tailFlushTimer = setTimeout(() => {
13248
+ tailFlushTimer = null;
13249
+ flushTail();
13250
+ }, MIRROR_TAIL_FLUSH_MS);
13251
+ }
13252
+ function mirrorLine(capped) {
13253
+ if (currentRunId === null) return;
13254
+ if (!headMirrorFull) {
13255
+ // Does THIS line fit, rather than "is the running total already over".
13256
+ // Both of the orderings tried before this were wrong in one direction
13257
+ // each: gating on the running total let one line of up to
13258
+ // HARD_PER_LINE_BYTES past a 5KB budget (mirrored head ~55KB, disagreeing
13259
+ // with collectStdout's head and with the "~7K chars" codeExecuteTool
13260
+ // advertises to the model), while adding first and checking after moved
13261
+ // the boundary but kept the crossing line in the head - so the tail was
13262
+ // still empty at the immediate flush below and a run killed right there
13263
+ // dropped the last line before the hang and reported itself complete.
13264
+ //
13265
+ // A fit check does both: the head stops at STDOUT_HEAD_BYTES exactly, and
13266
+ // the line that did not fit STARTS the tail, so the flush that fires on
13267
+ // this same call carries it.
13268
+ if (mirroredHeadBytes + capped.length + 1 <= STDOUT_HEAD_BYTES) {
13269
+ mirroredHeadBytes += capped.length + 1;
13270
+ postToMain({ type: 'stdout', id: currentRunId, chunk: capped });
13271
+ return;
13272
+ }
13273
+ headMirrorFull = true;
13274
+ tailChunks.push(capped);
13275
+ tailBytes += capped.length + 1;
13276
+ // Post once immediately: a run killed before the first timed flush would
13277
+ // otherwise report a short mirror as if it were complete.
13278
+ flushTail();
13279
+ return;
13280
+ }
13281
+ tailChunks.push(capped);
13282
+ tailBytes += capped.length + 1;
13283
+ // Never evict the only line held: a line larger than the whole budget is
13284
+ // still the last thing the run printed, which is what the mirror is for.
13285
+ const budget = tailBudget();
13286
+ while (tailBytes > budget && tailChunks.length > 1) {
13287
+ const dropped = tailChunks.shift();
13288
+ tailBytes -= dropped.length + 1;
13289
+ elidedBytes += dropped.length + 1;
13290
+ }
13291
+ scheduleTailFlush();
13292
+ }
12908
13293
  function captureLine(args) {
12909
13294
  const line = args.map(a => {
12910
13295
  if (typeof a === 'string') return a;
@@ -12916,7 +13301,7 @@ function captureLine(args) {
12916
13301
  ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
12917
13302
  : line;
12918
13303
  stdoutChunks.push(capped);
12919
- stdoutBytes += capped.length + 1;
13304
+ mirrorLine(capped);
12920
13305
  }
12921
13306
  function jsonReplacer(_k, v) {
12922
13307
  if (v instanceof Error) return { name: v.name, message: v.message };
@@ -13017,7 +13402,11 @@ parentPort.on('message', async (msg) => {
13017
13402
  }
13018
13403
  if (msg.type === 'runCode') {
13019
13404
  const t0 = Date.now();
13020
- stdoutChunks = []; stdoutBytes = 0; truncated = false;
13405
+ stdoutChunks = []; truncated = false;
13406
+ cancelTailFlush();
13407
+ currentRunId = msg.id;
13408
+ mirroredHeadBytes = 0; headMirrorFull = false;
13409
+ tailChunks = []; tailBytes = 0; elidedBytes = 0;
13021
13410
  let error = null;
13022
13411
  const wrapped = '(async () => {\n' + msg.code + '\n})()';
13023
13412
  try {
@@ -13029,6 +13418,11 @@ parentPort.on('message', async (msg) => {
13029
13418
  } catch (e) {
13030
13419
  error = serializeError(e);
13031
13420
  }
13421
+ // Stop mirroring before the authoritative result goes out, so a late
13422
+ // console.log from an abandoned continuation cannot attach to this run,
13423
+ // and a pending tail flush cannot land after it.
13424
+ currentRunId = null;
13425
+ cancelTailFlush();
13032
13426
  parentPort.postMessage({
13033
13427
  type: 'runResult',
13034
13428
  id: msg.id,
@@ -13042,6 +13436,17 @@ parentPort.on('message', async (msg) => {
13042
13436
  });
13043
13437
  `;
13044
13438
  String.raw`
13439
+ // Wrapped in an IIFE deliberately. A script's top-level const/let bind into the
13440
+ // context's SHARED global lexical scope (and its function declarations become
13441
+ // globalThis properties), so without this wrapper every bootstrap-local name is
13442
+ // directly referenceable by LLM-authored code run later in the same context:
13443
+ // __RealFunction('...')() walks straight around the codegen block below, and
13444
+ // __cap.applySync(...) / __cap.release() forges or permanently kills stdout
13445
+ // capture. Function scope keeps them unreachable. Note the leak is invisible to
13446
+ // listGlobals(), which reads Object.getOwnPropertyNames(globalThis) and never
13447
+ // saw the lexical bindings - so RESERVED_GLOBAL_NAMES cannot backstop it either.
13448
+ // Anything guest code IS meant to see is assigned onto globalThis explicitly.
13449
+ (function () {
13045
13450
  const __cap = _captureLine;
13046
13451
  const __callTool = _callTool;
13047
13452
  delete globalThis._captureLine;
@@ -13049,28 +13454,68 @@ delete globalThis._callTool;
13049
13454
 
13050
13455
  const HARD_PER_LINE_BYTES = ${5e4};
13051
13456
 
13457
+ // Every intrinsic the formatter below reaches for is captured HERE, while the
13458
+ // context is still pristine. Resolving \`args.map\` / \`.join\` / \`line.slice\`
13459
+ // at CALL time walks a prototype chain the guest owns, so one
13460
+ // \`Array.prototype.join = () => 'X'\` - deliberate, or an innocent polyfill -
13461
+ // forges every stdout line for the rest of the session, and the run still
13462
+ // reports error=null / truncated=false. That is the same integrity failure the
13463
+ // frozen \`console\` below exists to prevent, one level down: freezing the
13464
+ // binding is worthless if the formatter behind it is guest-reachable.
13465
+ const __stringify = JSON.stringify;
13466
+ const __String = String;
13467
+ const __apply = Reflect.apply;
13468
+ const __strSlice = String.prototype.slice;
13469
+
13052
13470
  function __jsonReplacer(_k, v) {
13053
13471
  if (v instanceof Error) return { name: v.name, message: v.message };
13054
13472
  if (typeof v === 'bigint') return v.toString() + 'n';
13055
13473
  return v;
13056
13474
  }
13475
+ // Indexed loop and \`+=\` rather than map/join: string concatenation is an
13476
+ // operator, not a lookup, so there is nothing here for the guest to replace.
13477
+ // What a guest CAN still steer is how its own values render - a \`toJSON\` or
13478
+ // \`toString\` on the object it passed - which is content it already owns, not
13479
+ // the channel.
13057
13480
  function __formatLine(args) {
13058
- const line = args.map(a => {
13059
- if (typeof a === 'string') return a;
13060
- if (a === undefined) return 'undefined';
13061
- if (a === null) return 'null';
13062
- try { return JSON.stringify(a, __jsonReplacer, 2); } catch { return String(a); }
13063
- }).join(' ');
13481
+ let line = '';
13482
+ for (let i = 0; i < args.length; i++) {
13483
+ if (i > 0) line += ' ';
13484
+ const a = args[i];
13485
+ if (typeof a === 'string') { line += a; continue; }
13486
+ if (a === undefined) { line += 'undefined'; continue; }
13487
+ if (a === null) { line += 'null'; continue; }
13488
+ try { line += __stringify(a, __jsonReplacer, 2); } catch { line += __String(a); }
13489
+ }
13064
13490
  return line.length > HARD_PER_LINE_BYTES
13065
- ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
13491
+ ? __apply(__strSlice, line, [0, HARD_PER_LINE_BYTES]) + ' [...line truncated]'
13066
13492
  : line;
13067
13493
  }
13068
- globalThis.console = {
13494
+ // stdout is the channel the HOST reports back as the run's observation, so its
13495
+ // integrity is ours, not the guest's. A plain assignment left \`console\`
13496
+ // writable and configurable: guest code could set globalThis.console = {log(){}}
13497
+ // (or just reassign console.log) and every later run in the session would come
13498
+ // back with stdout="" or forged lines, error=null, and a clean listGlobals().
13499
+ // Frozen object + non-writable, non-configurable property: the guest's
13500
+ // assignment is a silent no-op in sloppy mode and a TypeError under 'use
13501
+ // strict', and either way capture keeps working.
13502
+ //
13503
+ // The BINDING is what this protects, and the binding is only half of it: a
13504
+ // frozen console whose formatter resolved its intrinsics at call time would
13505
+ // still hand the guest every line. That half is closed above, where
13506
+ // __formatLine captures what it needs.
13507
+ const __console = Object.freeze({
13069
13508
  log: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
13070
13509
  warn: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
13071
13510
  error: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
13072
13511
  info: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
13073
- };
13512
+ });
13513
+ Object.defineProperty(globalThis, 'console', {
13514
+ value: __console,
13515
+ writable: false,
13516
+ configurable: false,
13517
+ enumerable: true,
13518
+ });
13074
13519
 
13075
13520
  // A bare isolate has no structuredClone (it's a host/web API, not a V8
13076
13521
  // intrinsic). The in-process + worker backends expose the *host's* real
@@ -13158,11 +13603,33 @@ for (const __Ctor of [__RealFunction, __AsyncFunction, __GeneratorFunction, __As
13158
13603
  globalThis.eval = __blockCodegen;
13159
13604
  globalThis.Function = __blockCodegen;
13160
13605
 
13606
+ // WebAssembly is removed, not stubbed. Its compile/instantiate promises never
13607
+ // settle inside an isolated-vm isolate (there is no host task runner to drive
13608
+ // them), so \`await WebAssembly.instantiate(...)\` is a one-line way for guest
13609
+ // code to park a run until the host deadline fires - and that deadline kills
13610
+ // the isolate, costing the whole session its sandbox. Deleting it turns that
13611
+ // into an immediate ReferenceError. It is also codegen-from-bytes, so it
13612
+ // belongs on the same side of the line as eval / Function anyway.
13613
+ delete globalThis.WebAssembly;
13614
+
13161
13615
  // Tool-stub registry. Each registered tool becomes a top-level async
13162
13616
  // function that round-trips through the host dispatcher and re-throws on
13163
13617
  // the { ok:false } envelope.
13618
+ //
13619
+ // Assigned to globalThis only so the constructor can lift a Reference to it;
13620
+ // the constructor deletes the global immediately afterwards and calls it
13621
+ // through that Reference forever after. It must NOT stay guest-reachable: a
13622
+ // guest could call __registerTools(['console']) to overwrite the frozen
13623
+ // console binding with a tool stub, or \`delete\` it and make the host's next
13624
+ // setTools() throw.
13625
+ //
13626
+ // Indexed loop, not for..of, deliberately: the host calls this with a copied
13627
+ // array whose iterator comes from the GUEST's Array.prototype, so an
13628
+ // overridden Symbol.iterator would let guest code hang or hijack a host-side
13629
+ // setTools() call. Indexing touches only the copy's own properties.
13164
13630
  globalThis.__registerTools = function (names) {
13165
- for (const name of names) {
13631
+ for (let i = 0; i < names.length; i++) {
13632
+ const name = names[i];
13166
13633
  globalThis[name] = async (...args) => {
13167
13634
  const envJson = await __callTool.apply(
13168
13635
  undefined,
@@ -13175,6 +13642,7 @@ globalThis.__registerTools = function (names) {
13175
13642
  };
13176
13643
  }
13177
13644
  };
13645
+ })();
13178
13646
  `;
13179
13647
  z$1.object({
13180
13648
  reflection: z$1.string().min(1),
@@ -15467,6 +15935,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15467
15935
  AnthropicBackend: () => AnthropicBackend,
15468
15936
  AnthropicBatchService: () => AnthropicBatchService,
15469
15937
  AnthropicBedrockBackend: () => AnthropicBedrockBackend,
15938
+ BEDROCK_REQUEST_HANDLER: () => BEDROCK_REQUEST_HANDLER,
15470
15939
  BFLBackend: () => BFLBackend,
15471
15940
  BaseBedrockBackend: () => BaseBedrockBackend,
15472
15941
  ChoiceEndReason: () => ChoiceEndReason,
@@ -15524,6 +15993,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15524
15993
  logExpiringModels: () => logExpiringModels,
15525
15994
  mergeCatalog: () => mergeCatalog,
15526
15995
  mergeCatalogWithDrops: () => mergeCatalogWithDrops,
15996
+ normalizeToolUseInputs: () => normalizeToolUseInputs,
15527
15997
  reasonsWithinOutputBudget: () => reasonsWithinOutputBudget,
15528
15998
  recordDeprecatedModelRequest: () => recordDeprecatedModelRequest,
15529
15999
  replaceLastToolResultObservationCanonical: () => replaceLastToolResultObservationCanonical,
@@ -15898,27 +16368,112 @@ const stripAllToolBlocks = (messages, logger) => {
15898
16368
  return result;
15899
16369
  };
15900
16370
  /**
16371
+ * Restores `input: {}` on any `tool_use` block that reached us without one.
16372
+ *
16373
+ * `MessageContentToolUse.input` is non-optional in the type system, so nothing upstream checks it -
16374
+ * but a message that round-trips through a persistence or serialization layer can lose it. The
16375
+ * known offender is Mongoose's default `minimize`, which deletes empty objects on
16376
+ * `toObject()`/`toJSON()`: a zero-argument tool call (`current_datetime`, `mission_status`, ...)
16377
+ * stores `input: {}` and reads back with the key gone. Anthropic then rejects the whole request
16378
+ * with "messages.N.content.M.tool_use.input: Field required", killing a resumed agent run or a
16379
+ * chat turn that replays history.
16380
+ *
16381
+ * The schema that caused it is fixed at the source (`minimize: false` on AgentExecutionModel), so
16382
+ * this is the last line of defense for every other store that replays blocks verbatim -
16383
+ * `QuestModel.structuredReplies[].content` is the same Mixed-under-default-minimize shape and is
16384
+ * deliberately covered here rather than by widening that hot collection's schema. The cost of a
16385
+ * miss is a hard 400, and `{}` is the only value a zero-argument call could have had.
16386
+ *
16387
+ * Returns the input array unchanged (same reference) when nothing needed repair.
16388
+ */
16389
+ const normalizeToolUseInputs = (messages, logger) => {
16390
+ let repaired = 0;
16391
+ const result = messages.map((message) => {
16392
+ if (!Array.isArray(message.content)) return message;
16393
+ let messageChanged = false;
16394
+ const content = message.content.map((block) => {
16395
+ if (block.type !== "tool_use") return block;
16396
+ const toolUse = block;
16397
+ if (toolUse.input !== null && typeof toolUse.input === "object") return block;
16398
+ repaired++;
16399
+ messageChanged = true;
16400
+ return {
16401
+ ...toolUse,
16402
+ input: {}
16403
+ };
16404
+ });
16405
+ return messageChanged ? {
16406
+ ...message,
16407
+ content
16408
+ } : message;
16409
+ });
16410
+ if (repaired === 0) return messages;
16411
+ logger?.warn(`[Tool Input Repair] Restored empty input on ${repaired} tool_use block(s) that lost it in serialization`);
16412
+ return result;
16413
+ };
16414
+ /**
16415
+ * Anthropic's hard ceiling on `cache_control` markers per request. Exceeding it fails the
16416
+ * WHOLE request with `ValidationException: A maximum of 4 blocks with cache_control may be
16417
+ * provided`, which is non-retryable - so an over-budget request loses the turn outright,
16418
+ * after the user has already waited for it.
16419
+ */
16420
+ const MAX_CACHE_CONTROL_BLOCKS = 4;
16421
+ /**
16422
+ * Does this block already carry a marker? Re-marking one costs no budget.
16423
+ *
16424
+ * Tests the VALUE, not just key presence: a block carrying an explicit
16425
+ * `cache_control: undefined` is not a marker as far as the provider is concerned, and counting
16426
+ * it would spend budget on nothing and drop a breakpoint we could have kept.
16427
+ */
16428
+ function hasMarker(block) {
16429
+ return !!block && typeof block === "object" && !!block.cache_control;
16430
+ }
16431
+ /**
16432
+ * Markers already on the request. Callers upstream attach their own before this runs -
16433
+ * `bedrockBackend/anthropic.ts` marks each system block flagged `cache: true` (the mid-stack
16434
+ * shareable-prefix breakpoint) - so this adapter's budget is whatever they left, not the full four.
16435
+ */
16436
+ function censusMarkers(params) {
16437
+ const tools = Array.isArray(params.tools) ? params.tools.filter(hasMarker).length : 0;
16438
+ const system = Array.isArray(params.system) ? params.system.filter(hasMarker).length : 0;
16439
+ let messages = 0;
16440
+ if (Array.isArray(params.messages)) for (const message of params.messages) {
16441
+ const content = message?.content;
16442
+ if (Array.isArray(content)) messages += content.filter(hasMarker).length;
16443
+ }
16444
+ return {
16445
+ tools,
16446
+ system,
16447
+ messages,
16448
+ total: tools + system + messages
16449
+ };
16450
+ }
16451
+ /**
15901
16452
  * Anthropic-specific caching adapter
15902
16453
  * Adds explicit cache_control markers to content blocks
15903
16454
  */
15904
16455
  var AnthropicCachingAdapter = class {
15905
- applyCaching(apiParams, strategy) {
16456
+ applyCaching(apiParams, strategy, logger) {
15906
16457
  if (!strategy.enableCaching) return apiParams;
15907
16458
  const ttl = strategy.cacheTTL ?? "5m";
15908
16459
  const modifiedParams = { ...apiParams };
15909
- const tools = modifiedParams.tools;
15910
- if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
15911
- const toolsCopy = [...tools];
15912
- const lastTool = toolsCopy[toolsCopy.length - 1];
15913
- toolsCopy[toolsCopy.length - 1] = {
15914
- ...lastTool,
15915
- cache_control: {
15916
- type: "ephemeral",
15917
- ...ttl === "1h" ? { ttl } : {}
15918
- }
15919
- };
15920
- modifiedParams.tools = toolsCopy;
15921
- }
16460
+ const cacheControl = {
16461
+ type: "ephemeral",
16462
+ ...ttl === "1h" ? { ttl } : {}
16463
+ };
16464
+ const inbound = censusMarkers(modifiedParams);
16465
+ let budget = MAX_CACHE_CONTROL_BLOCKS - inbound.total;
16466
+ const dropped = [];
16467
+ /** Claim one marker slot, or record the miss. Re-marking a marked block is free. */
16468
+ const claim = (name, alreadyMarked) => {
16469
+ if (alreadyMarked) return true;
16470
+ if (budget <= 0) {
16471
+ dropped.push(name);
16472
+ return false;
16473
+ }
16474
+ budget -= 1;
16475
+ return true;
16476
+ };
15922
16477
  const systemParam = modifiedParams.system;
15923
16478
  if (strategy.cacheSystemPrompt && systemParam) {
15924
16479
  const systemArray = Array.isArray(systemParam) ? [...systemParam] : [{
@@ -15927,14 +16482,13 @@ var AnthropicCachingAdapter = class {
15927
16482
  }];
15928
16483
  if (systemArray.length > 0) {
15929
16484
  const lastBlock = systemArray[systemArray.length - 1];
15930
- systemArray[systemArray.length - 1] = {
15931
- ...lastBlock,
15932
- cache_control: {
15933
- type: "ephemeral",
15934
- ...ttl === "1h" ? { ttl } : {}
15935
- }
15936
- };
15937
- modifiedParams.system = systemArray;
16485
+ if (claim("system", hasMarker(lastBlock))) {
16486
+ systemArray[systemArray.length - 1] = {
16487
+ ...lastBlock,
16488
+ cache_control: cacheControl
16489
+ };
16490
+ modifiedParams.system = systemArray;
16491
+ }
15938
16492
  }
15939
16493
  }
15940
16494
  const messagesParam = modifiedParams.messages;
@@ -15950,24 +16504,65 @@ var AnthropicCachingAdapter = class {
15950
16504
  text: msgContent
15951
16505
  }];
15952
16506
  else if (Array.isArray(msgContent)) contentArray = [...msgContent];
15953
- else return modifiedParams;
15954
- if (contentArray.length > 0) {
16507
+ if (contentArray && contentArray.length > 0) {
15955
16508
  const lastBlock = contentArray[contentArray.length - 1];
15956
- contentArray[contentArray.length - 1] = {
15957
- ...lastBlock,
15958
- cache_control: {
15959
- type: "ephemeral",
15960
- ...ttl === "1h" ? { ttl } : {}
15961
- }
15962
- };
15963
- messages[anchorIndex] = {
15964
- ...anchorMsg,
15965
- content: contentArray
15966
- };
15967
- modifiedParams.messages = messages;
16509
+ if (claim("history", hasMarker(lastBlock))) {
16510
+ contentArray[contentArray.length - 1] = {
16511
+ ...lastBlock,
16512
+ cache_control: cacheControl
16513
+ };
16514
+ messages[anchorIndex] = {
16515
+ ...anchorMsg,
16516
+ content: contentArray
16517
+ };
16518
+ modifiedParams.messages = messages;
16519
+ }
15968
16520
  }
15969
16521
  }
15970
16522
  }
16523
+ const tools = modifiedParams.tools;
16524
+ if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
16525
+ const toolsCopy = [...tools];
16526
+ const lastTool = toolsCopy[toolsCopy.length - 1];
16527
+ if (claim("tools", hasMarker(lastTool))) {
16528
+ toolsCopy[toolsCopy.length - 1] = {
16529
+ ...lastTool,
16530
+ cache_control: cacheControl
16531
+ };
16532
+ modifiedParams.tools = toolsCopy;
16533
+ }
16534
+ }
16535
+ const outbound = censusMarkers(modifiedParams);
16536
+ const census = {
16537
+ inbound,
16538
+ outbound,
16539
+ limit: MAX_CACHE_CONTROL_BLOCKS
16540
+ };
16541
+ if (outbound.total >= MAX_CACHE_CONTROL_BLOCKS) {
16542
+ const message = "[PromptCache] cache_control census at the ceiling";
16543
+ if (logger) logger.info(message, census);
16544
+ else console.info(message, JSON.stringify(census));
16545
+ } else if (logger) logger.debug("[PromptCache] cache_control census", census);
16546
+ if (outbound.total > MAX_CACHE_CONTROL_BLOCKS) {
16547
+ const message = `[PromptCache] request exceeds the ${MAX_CACHE_CONTROL_BLOCKS}-block cache_control limit on arrival (${outbound.total}); the provider will reject it`;
16548
+ const detail = {
16549
+ inbound,
16550
+ outbound,
16551
+ limit: MAX_CACHE_CONTROL_BLOCKS
16552
+ };
16553
+ if (logger) logger.error(message, detail);
16554
+ else console.error(message, JSON.stringify(detail));
16555
+ } else if (dropped.length > 0) {
16556
+ const message = `[PromptCache] cache_control budget exhausted (limit ${MAX_CACHE_CONTROL_BLOCKS}); skipped breakpoints: ${dropped.join(", ")}`;
16557
+ const detail = {
16558
+ dropped,
16559
+ inbound,
16560
+ outbound,
16561
+ limit: MAX_CACHE_CONTROL_BLOCKS
16562
+ };
16563
+ if (logger) logger.warn(message, detail);
16564
+ else console.warn(message, JSON.stringify(detail));
16565
+ }
15971
16566
  return modifiedParams;
15972
16567
  }
15973
16568
  extractCacheStats(response, model) {
@@ -17095,7 +17690,7 @@ var AnthropicBackend = class {
17095
17690
  const parts = [this.consolidateSystemMessages(messages), identityReminder].filter(Boolean);
17096
17691
  system = parts.length > 0 ? parts.join("\n") : void 0;
17097
17692
  }
17098
- let filteredMessages = ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger);
17693
+ let filteredMessages = normalizeToolUseInputs(ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger), this.logger);
17099
17694
  const countToolBlocks = (msgs) => {
17100
17695
  let useCount = 0;
17101
17696
  let resultCount = 0;
@@ -17195,7 +17790,7 @@ var AnthropicBackend = class {
17195
17790
  } else this.isThinkingEnabled = false;
17196
17791
  const cacheStrategy = options.cacheStrategy;
17197
17792
  if (cacheStrategy?.enableCaching) {
17198
- const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy);
17793
+ const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy, this.logger);
17199
17794
  Object.assign(apiParams, cachedParams);
17200
17795
  this.logger.debug("[Anthropic] Applying cache control", {
17201
17796
  cacheSystemPrompt: cacheStrategy.cacheSystemPrompt,
@@ -18219,6 +18814,11 @@ const BEDROCK_RETRY_CONFIG = {
18219
18814
  maxAttempts: 6,
18220
18815
  retryMode: "adaptive"
18221
18816
  };
18817
+ const BEDROCK_REQUEST_HANDLER = {
18818
+ requestTimeout: 12e4,
18819
+ sessionTimeout: 13e4,
18820
+ disableConcurrentStreams: true
18821
+ };
18222
18822
  /**
18223
18823
  * Detect cancellation errors so they propagate past tool-error containment to
18224
18824
  * the outer catch (which has dedicated abort handling). Without this, aborts
@@ -18260,7 +18860,8 @@ var BaseBedrockBackend = class {
18260
18860
  };
18261
18861
  this._bedrockRuntime = new BedrockRuntimeClient({
18262
18862
  region: this._options.region,
18263
- ...BEDROCK_RETRY_CONFIG
18863
+ ...BEDROCK_RETRY_CONFIG,
18864
+ requestHandler: BEDROCK_REQUEST_HANDLER
18264
18865
  });
18265
18866
  }
18266
18867
  getRegionForModel(model) {
@@ -18290,12 +18891,31 @@ var BaseBedrockBackend = class {
18290
18891
  takeReasoningBlocks() {
18291
18892
  return [];
18292
18893
  }
18894
+ /**
18895
+ * Whether this adapter's `translateStreamChunk` reports `done: true` ONLY on the provider's
18896
+ * terminal event. When true, complete() treats a stream that produced output but never
18897
+ * reported done as a TRUNCATED response and throws instead of returning the partial text.
18898
+ *
18899
+ * Opt-in rather than the default because "reports done terminally" is a per-adapter contract
18900
+ * the base class cannot infer, and getting it wrong turns every healthy completion into an
18901
+ * error. Three groups exist today:
18902
+ * - terminal-only, so they override this to true: anthropic, deepseek, llama, jurassicTwo
18903
+ * - `done: true` on EVERY content chunk, so the check would be inert: titan, moonshot
18904
+ * (the better fix for those is a stopReason passthrough, as moonshot.ts already does)
18905
+ * - never report done, incl. the test doubles in this directory: left false
18906
+ *
18907
+ * A new streaming backend must opt in deliberately; silence keeps the old behaviour.
18908
+ */
18909
+ get signalsStreamTermination() {
18910
+ return false;
18911
+ }
18293
18912
  updateClientForModel(model) {
18294
18913
  const requiredRegion = this.getRegionForModel(model);
18295
18914
  this._options.region = requiredRegion;
18296
18915
  this._bedrockRuntime = new BedrockRuntimeClient({
18297
18916
  region: this._options.region,
18298
- ...BEDROCK_RETRY_CONFIG
18917
+ ...BEDROCK_RETRY_CONFIG,
18918
+ requestHandler: BEDROCK_REQUEST_HANDLER
18299
18919
  });
18300
18920
  }
18301
18921
  async complete(model, messages, options, callback, toolsUsed = []) {
@@ -18403,9 +19023,11 @@ var BaseBedrockBackend = class {
18403
19023
  if (!response.body) throw new Error("No response body");
18404
19024
  const func = [];
18405
19025
  let emittedTextChars = 0;
19026
+ let sawTerminalEvent = false;
18406
19027
  for await (const streamEvent of response.body) if (streamEvent.chunk?.bytes) {
18407
19028
  const json = new TextDecoder().decode(streamEvent.chunk.bytes);
18408
- const { chunk } = this.translateStreamChunk(model, JSON.parse(json));
19029
+ const { done, chunk } = this.translateStreamChunk(model, JSON.parse(json));
19030
+ sawTerminalEvent ||= done;
18409
19031
  if (chunk?.stopReason) stopReason = chunk.stopReason;
18410
19032
  chunk?.choices?.forEach((choice) => {
18411
19033
  func[choice.index] ||= {};
@@ -18429,6 +19051,7 @@ var BaseBedrockBackend = class {
18429
19051
  await callback(streamedText, buildCompletionInfo());
18430
19052
  }
18431
19053
  if (emittedTextChars === 0 && !func.some((f) => f.name)) throw new Error(`[BaseBedrockBackend] model "${model}" returned an EMPTY response in region ${this._options.region} (no text, no tool call, no output tokens). A "global." cross-region inference profile served from a region that does not host it does exactly this - try the "us." variant, or confirm the model/profile is granted in ${this._options.region}.`);
19054
+ if (this.signalsStreamTermination && !sawTerminalEvent && !options.abortSignal?.aborted) throw new Error(`[BaseBedrockBackend] stream timeout - model "${model}" in region ${this._options.region} ended after ${emittedTextChars} chars without a terminal event, so the response is TRUNCATED. Usually a stalled Bedrock socket cut the stream short; the partial text is withheld deliberately rather than returned as a finished answer.`);
18432
19055
  if (func.some((f) => f.name)) {
18433
19056
  for await (const tool of func) {
18434
19057
  const { id, name, parameters } = tool;
@@ -18733,6 +19356,10 @@ const TEMPERATURE_ONLY_MODELS = [
18733
19356
  ChatModels.CLAUDE_4_6_OPUS_BEDROCK
18734
19357
  ];
18735
19358
  var AnthropicBedrockBackend = class extends BaseBedrockBackend {
19359
+ /** Reports done only on message_stop (anthropic.ts translateStreamChunk), so a missing terminal event means a truncated stream. */
19360
+ get signalsStreamTermination() {
19361
+ return true;
19362
+ }
18736
19363
  isInThinkingBlock = false;
18737
19364
  /**
18738
19365
  * Reasoning blocks of the assistant turn currently being translated, indexed by the
@@ -19495,6 +20122,10 @@ function isReasoningBlock(block) {
19495
20122
  return "reasoningContent" in block;
19496
20123
  }
19497
20124
  var DeepSeekBedrockBackend = class extends BaseBedrockBackend {
20125
+ /** Reports done only on event.messageStop, so a missing terminal event means a truncated stream. */
20126
+ get signalsStreamTermination() {
20127
+ return true;
20128
+ }
19498
20129
  /** Suppresses reasoning/thinking output for summary and title generation calls. */
19499
20130
  isSpecialTask = false;
19500
20131
  /** Tracks whether the stream is currently inside a reasoning span, to emit one <think>/</think> pair per span. */
@@ -19839,6 +20470,10 @@ var JurassicTwoBedrockBackend = class extends BaseBedrockBackend {
19839
20470
  }
19840
20471
  };
19841
20472
  var LlamaBedrockBackend = class extends BaseBedrockBackend {
20473
+ /** Reports done only on response.stop_reason on the terminal chunk, so a missing terminal event means a truncated stream. */
20474
+ get signalsStreamTermination() {
20475
+ return true;
20476
+ }
19842
20477
  async getModelInfo() {
19843
20478
  return [
19844
20479
  {
@@ -20984,7 +21619,7 @@ var GeminiBackend = class {
20984
21619
  rank: 8,
20985
21620
  trainingCutoff: "2025-01-31",
20986
21621
  releaseDate: "2025-06-01",
20987
- deprecationDate: "2026-10-16",
21622
+ deprecationDate: "2026-09-02",
20988
21623
  description: "Google's Gemini 2.5 Flash, offering well-rounded price-performance. Best for large scale processing, low-latency, high volume tasks that require thinking, and agentic use cases"
20989
21624
  },
20990
21625
  {
@@ -25949,6 +26584,7 @@ const DEPRECATED_MODEL_MAP = {
25949
26584
  "claude-3-haiku-20240307": "claude-haiku-4-5-20251001",
25950
26585
  "gpt-5-chat-latest": "gpt-5.5",
25951
26586
  "gpt-5.1-chat-latest": "gpt-5.5",
26587
+ "gemini-2.5-flash": "gemini-3.1-flash-lite",
25952
26588
  "grok-3": "grok-4.5",
25953
26589
  "grok-3-fast": "grok-4.5",
25954
26590
  "grok-2-1212": "grok-4.5",
@@ -27522,6 +28158,109 @@ async function createSseBackend(input, deps = defaultSseTransportDeps) {
27522
28158
  }
27523
28159
  //#endregion
27524
28160
  //#region ../../b4m-core/mcp/dist/index.mjs
28161
+ /**
28162
+ * Environment construction for the MCP stdio child process.
28163
+ *
28164
+ * The child is spawned from a process that also holds platform credentials - provider API keys,
28165
+ * database URIs, signing secrets - so what it inherits is a trust decision, not a convenience.
28166
+ * Two rules follow:
28167
+ *
28168
+ * 1. The child environment is built from an allowlist, never spread from `process.env`. The base
28169
+ * layer is the MCP SDK's own `getDefaultEnvironment()`, which the stdio transport merges
28170
+ * underneath whatever we pass (PATH, HOME, SHELL, TERM, USER on POSIX; the equivalent set on
28171
+ * Windows). Everything above that base comes from the table below.
28172
+ * 2. A stored variable is provider data, never runtime configuration. The child is a Node
28173
+ * process, so a key like NODE_OPTIONS is applied by the runtime before a single line of
28174
+ * server code loads: `--require /tmp/x.js` would turn a credential field into arbitrary code
28175
+ * execution inside the child. Those keys are refused rather than dropped quietly.
28176
+ *
28177
+ * MUST STAY IN SYNC with the `process.env` reads under each server directory
28178
+ * (`github/config.ts`, `notion/config.ts`, `atlassian/config.ts`, `linkedin/index.ts`). A
28179
+ * variable a server reads but this table omits arrives `undefined`, so add it here in the same
28180
+ * change. `childEnv.test.ts` pins that both ways.
28181
+ */
28182
+ const MCP_SERVER_ENV_KEYS = {
28183
+ [McpServerName.LinkedIn]: ["LINKEDIN_ACCESS_TOKEN", "COMPANY_NAME"],
28184
+ [McpServerName.Github]: ["GITHUB_ACCESS_TOKEN"],
28185
+ [McpServerName.Atlassian]: [
28186
+ "ATLASSIAN_ACCESS_TOKEN",
28187
+ "ATLASSIAN_CLOUD_ID",
28188
+ "ATLASSIAN_SITE_URL"
28189
+ ],
28190
+ [McpServerName.Notion]: [
28191
+ "NOTION_ACCESS_TOKEN",
28192
+ "NOTION_WORKSPACE_ID",
28193
+ "NOTION_WRITE_ENABLED",
28194
+ "NOTION_ROOT_PAGE_ID",
28195
+ "NOTION_ACCESS_MODE",
28196
+ "NOTION_ALLOWED_PAGES",
28197
+ "NOTION_EXCLUDED_PAGE_IDS",
28198
+ "NOTION_DEBUG"
28199
+ ]
28200
+ };
28201
+ /**
28202
+ * Keys that make the runtime execute caller-chosen code before the server's entry point runs:
28203
+ * NODE_OPTIONS can `--require` a file, the loader variables preload a shared object, and
28204
+ * ELECTRON_RUN_AS_NODE changes what the binary is. Matching is case-insensitive because Windows
28205
+ * environment names are.
28206
+ */
28207
+ const CODE_INJECTING_ENV_KEY_PATTERNS = [
28208
+ /^NODE_/i,
28209
+ /^ELECTRON_RUN_AS_NODE$/i,
28210
+ /^LD_/i,
28211
+ /^DYLD_/i
28212
+ ];
28213
+ /**
28214
+ * Keys that steer where the child resolves things rather than what it executes: npm_* redirects
28215
+ * package resolution, PATH decides which binary a bare command name finds, and the proxy
28216
+ * variables redirect outbound traffic.
28217
+ */
28218
+ const RESOLUTION_STEERING_ENV_KEY_PATTERNS = [
28219
+ /^npm_/i,
28220
+ /^PATH$/i,
28221
+ /^PATHEXT$/i,
28222
+ /^(HTTP|HTTPS|ALL|NO|FTP)_PROXY$/i,
28223
+ /^GLOBAL_AGENT_/i
28224
+ ];
28225
+ [...CODE_INJECTING_ENV_KEY_PATTERNS, ...RESOLUTION_STEERING_ENV_KEY_PATTERNS];
28226
+ const matchesAny = (patterns, key) => {
28227
+ const normalized = key.trim();
28228
+ return patterns.some((pattern) => pattern.test(normalized));
28229
+ };
28230
+ /** True when `key` would have the runtime load caller-chosen code before the server starts. */
28231
+ function isCodeInjectingMcpEnvKey(key) {
28232
+ return matchesAny(CODE_INJECTING_ENV_KEY_PATTERNS, key);
28233
+ }
28234
+ /**
28235
+ * Build the environment for a stdio MCP child.
28236
+ *
28237
+ * A bundled server gets exactly its declared variables - the allowlist decides, and the denylist
28238
+ * above is never consulted.
28239
+ *
28240
+ * A caller-defined command has no declared contract to check against, so it gets everything
28241
+ * except the code-injecting keys. Only the `b4m` CLI config reaches this branch, and that file
28242
+ * already lets its owner set `command` and `args` to any binary - so withholding PATH or a proxy
28243
+ * variable from them protects nobody while breaking a wrapper script or a corporate proxy, and
28244
+ * the warning that says so goes to a stderr the TUI hides. The code-injecting half stays because
28245
+ * an env-only `--require` is the one lever that is easy to set by accident.
28246
+ */
28247
+ function buildMcpChildEnv({ serverName, envVariables, hasCustomCommand = false }) {
28248
+ const declaredKeys = hasCustomCommand ? void 0 : MCP_SERVER_ENV_KEYS[serverName];
28249
+ const isAllowed = declaredKeys ? (key) => declaredKeys.includes(key) : (key) => !isCodeInjectingMcpEnvKey(key);
28250
+ const env = {};
28251
+ const droppedKeys = [];
28252
+ for (const { key, value } of envVariables) {
28253
+ if (!isAllowed(key)) {
28254
+ droppedKeys.push(key);
28255
+ continue;
28256
+ }
28257
+ env[key] = value;
28258
+ }
28259
+ return {
28260
+ env,
28261
+ droppedKeys
28262
+ };
28263
+ }
27525
28264
  var MCPClient = class {
27526
28265
  mcp;
27527
28266
  transport = null;
@@ -27565,14 +28304,11 @@ var MCPClient = class {
27565
28304
  }));
27566
28305
  return;
27567
28306
  }
27568
- const envVarsObject = this.envVariables.reduce((acc, env) => ({
27569
- ...acc,
27570
- [env.key]: env.value
27571
- }), {});
27572
28307
  let command;
27573
28308
  let args;
27574
- if (this.customCommand && this.customCommand.trim() !== "") {
27575
- command = this.customCommand;
28309
+ const customCommand = this.customCommand?.trim() ? this.customCommand : void 0;
28310
+ if (customCommand) {
28311
+ command = customCommand;
27576
28312
  args = this.customArgs ?? [];
27577
28313
  } else {
27578
28314
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -27589,13 +28325,16 @@ var MCPClient = class {
27589
28325
  console.log(`[MCP] Using server: ${this.serverName} at ${serverScriptPath}`);
27590
28326
  }
27591
28327
  const stderrMode = this.suppressStderr ? "ignore" : this.onStderrLine ? "pipe" : void 0;
28328
+ const { env, droppedKeys } = buildMcpChildEnv({
28329
+ serverName: this.serverName,
28330
+ envVariables: this.envVariables,
28331
+ hasCustomCommand: Boolean(customCommand)
28332
+ });
28333
+ if (droppedKeys.length > 0) console.warn(`[MCP] Withheld ${droppedKeys.length} undeclared env variable(s) from ${this.serverName}: ${droppedKeys.join(", ")}`);
27592
28334
  const transportConfig = {
27593
28335
  command,
27594
28336
  args,
27595
- env: {
27596
- ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0)),
27597
- ...envVarsObject
27598
- },
28337
+ env,
27599
28338
  ...stderrMode && { stderr: stderrMode }
27600
28339
  };
27601
28340
  const stdioTransport = new StdioClientTransport(transportConfig);
@@ -28023,7 +28762,7 @@ const MODEL_ALIASES = {
28023
28762
  "o4-mini": ChatModels.O4_MINI,
28024
28763
  gemini: ChatModels.GEMINI_2_5_PRO,
28025
28764
  "gemini-pro": ChatModels.GEMINI_2_5_PRO,
28026
- "gemini-flash": ChatModels.GEMINI_2_5_FLASH,
28765
+ "gemini-flash": ChatModels.GEMINI_3_5_FLASH,
28027
28766
  "gemini-flash-lite": ChatModels.GEMINI_2_5_FLASH_LITE,
28028
28767
  "gemini-3": ChatModels.GEMINI_3_PRO_PREVIEW,
28029
28768
  "gemini-3-pro": ChatModels.GEMINI_3_PRO_PREVIEW,
@@ -28458,7 +29197,7 @@ function buildFilenameMarkerRegex(markers) {
28458
29197
  * of the best-effort DB pre-filter. Fail-closed by design.
28459
29198
  */
28460
29199
  function isRetrievalExcluded(file, opts) {
28461
- const stalledByConvergence = isConvergencePausedNote(file.notes) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
29200
+ const stalledByConvergence = isChunkStalledFile(file) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
28462
29201
  if (opts.vectorizedOnly && !file.vectorized && !stalledByConvergence) return true;
28463
29202
  const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
28464
29203
  return !!re && re.test((file.fileName ?? "").toLowerCase());
@@ -29042,6 +29781,18 @@ function attachedContentBudgetsAgree(maxSafeInputTokens, systemPromptReserve) {
29042
29781
  var AdminSettingsCache = class AdminSettingsCache {
29043
29782
  cache = /* @__PURE__ */ new Map();
29044
29783
  individualCache = /* @__PURE__ */ new Map();
29784
+ /**
29785
+ * Every call through this field is optional-chained (`this.logger.debug?.()`).
29786
+ *
29787
+ * A cache must not throw because it could not log, and this one is exposed to that: it is a
29788
+ * process-wide singleton created with whichever logger happens to reach `getSettingsCache` first.
29789
+ * What each caller then does with a throw varies, and it is mostly NOT a degrade-to-defaults
29790
+ * guard: `getSettingsByNames` has none at all, the scoped resolver guards one layer out in
29791
+ * `resolveAll`, and `resolveSpendLevers` deliberately rethrows to halt spend. So a logger missing
29792
+ * a quieter level could surface as a silent wrong VALUE, as an unhandled rejection, or as a hard
29793
+ * fail-closed, depending on who asked. `ScopedSettingsCache` is built by the same factory pair
29794
+ * and still has one unguarded call - the same hazard, not a solved one.
29795
+ */
29045
29796
  logger;
29046
29797
  cleanupInterval = null;
29047
29798
  maxCacheSize = 1e3;
@@ -29057,13 +29808,13 @@ var AdminSettingsCache = class AdminSettingsCache {
29057
29808
  */
29058
29809
  startCleanupTimer() {
29059
29810
  if (process.env.NODE_ENV !== "production" || process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
29060
- this.logger.debug("Skipping cleanup timer in serverless environment");
29811
+ this.logger.debug?.("Skipping cleanup timer in serverless environment");
29061
29812
  return;
29062
29813
  }
29063
29814
  this.cleanupInterval = setInterval(() => {
29064
29815
  this.performCleanup();
29065
29816
  }, AdminSettingsCache.CLEANUP_INTERVAL);
29066
- this.logger.debug("Started cache cleanup timer");
29817
+ this.logger.debug?.("Started cache cleanup timer");
29067
29818
  }
29068
29819
  /**
29069
29820
  * Stop cleanup timer (for graceful shutdown)
@@ -29072,7 +29823,7 @@ var AdminSettingsCache = class AdminSettingsCache {
29072
29823
  if (this.cleanupInterval) {
29073
29824
  clearInterval(this.cleanupInterval);
29074
29825
  this.cleanupInterval = null;
29075
- this.logger.debug("Stopped cache cleanup timer");
29826
+ this.logger.debug?.("Stopped cache cleanup timer");
29076
29827
  }
29077
29828
  }
29078
29829
  /**
@@ -29097,9 +29848,9 @@ var AdminSettingsCache = class AdminSettingsCache {
29097
29848
  this.individualCache.delete(entries[i][0]);
29098
29849
  removedCount++;
29099
29850
  }
29100
- this.logger.warn(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
29851
+ this.logger.warn?.(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
29101
29852
  }
29102
- if (removedCount > 0) this.logger.debug(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
29853
+ if (removedCount > 0) this.logger.debug?.(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
29103
29854
  }
29104
29855
  /**
29105
29856
  * Get TTL based on environment
@@ -29120,18 +29871,18 @@ var AdminSettingsCache = class AdminSettingsCache {
29120
29871
  const cacheKey = "all_settings";
29121
29872
  const cached = this.cache.get(cacheKey);
29122
29873
  if (cached && this.isValid(cached.timestamp, cached.ttl)) {
29123
- this.logger.debug("📦 Admin settings cache HIT");
29874
+ this.logger.debug?.("📦 Admin settings cache HIT");
29124
29875
  return cached.data;
29125
29876
  }
29126
29877
  if (cached) this.cache.delete(cacheKey);
29127
- this.logger.debug("🔍 Admin settings cache MISS - fetching from database");
29878
+ this.logger.debug?.("🔍 Admin settings cache MISS - fetching from database");
29128
29879
  const fetchStart = Date.now();
29129
29880
  const settingsMap = (await db.adminSettings.findAll()).reduce((out, s) => {
29130
29881
  out[s.settingName] = s.settingValue;
29131
29882
  return out;
29132
29883
  }, {});
29133
29884
  const fetchTime = Date.now() - fetchStart;
29134
- this.logger.info(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
29885
+ this.logger.info?.(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
29135
29886
  const ttl = this.getTTL();
29136
29887
  this.cache.set(cacheKey, {
29137
29888
  data: settingsMap,
@@ -29153,15 +29904,15 @@ var AdminSettingsCache = class AdminSettingsCache {
29153
29904
  async getSettingByName(settingName, db) {
29154
29905
  const cached = this.individualCache.get(settingName);
29155
29906
  if (cached && this.isValid(cached.timestamp, cached.ttl)) {
29156
- this.logger.debug(`📦 Individual setting '${settingName}' cache HIT`);
29907
+ this.logger.debug?.(`📦 Individual setting '${settingName}' cache HIT`);
29157
29908
  return cached.value;
29158
29909
  }
29159
29910
  if (cached) this.individualCache.delete(settingName);
29160
- this.logger.debug(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
29911
+ this.logger.debug?.(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
29161
29912
  const fetchStart = Date.now();
29162
29913
  const value = (await db.adminSettings.findBySettingName(settingName))?.settingValue ?? null;
29163
29914
  const fetchTime = Date.now() - fetchStart;
29164
- this.logger.debug(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
29915
+ this.logger.debug?.(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
29165
29916
  this.individualCache.set(settingName, {
29166
29917
  value,
29167
29918
  timestamp: Date.now(),
@@ -29185,11 +29936,11 @@ var AdminSettingsCache = class AdminSettingsCache {
29185
29936
  }
29186
29937
  }
29187
29938
  if (uncachedSettings.length > 0) {
29188
- this.logger.debug(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
29939
+ this.logger.debug?.(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
29189
29940
  const fetchStart = Date.now();
29190
29941
  const settings = await db.adminSettings.findBySettingNames(uncachedSettings);
29191
29942
  const fetchTime = Date.now() - fetchStart;
29192
- this.logger.debug(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
29943
+ this.logger.debug?.(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
29193
29944
  const ttl = this.getTTL();
29194
29945
  settings.forEach((setting) => {
29195
29946
  result[setting.settingName] = setting.settingValue;
@@ -29207,7 +29958,7 @@ var AdminSettingsCache = class AdminSettingsCache {
29207
29958
  });
29208
29959
  });
29209
29960
  }
29210
- this.logger.debug(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
29961
+ this.logger.debug?.(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
29211
29962
  return result;
29212
29963
  }
29213
29964
  /**
@@ -29216,7 +29967,7 @@ var AdminSettingsCache = class AdminSettingsCache {
29216
29967
  invalidateSetting(settingName) {
29217
29968
  this.individualCache.delete(settingName);
29218
29969
  this.cache.delete("all_settings");
29219
- this.logger.info(`🗑️ Invalidated cache for setting: ${settingName}`);
29970
+ this.logger.info?.(`🗑️ Invalidated cache for setting: ${settingName}`);
29220
29971
  }
29221
29972
  /**
29222
29973
  * Invalidate all cached admin settings
@@ -29224,7 +29975,7 @@ var AdminSettingsCache = class AdminSettingsCache {
29224
29975
  invalidateAll() {
29225
29976
  this.cache.clear();
29226
29977
  this.individualCache.clear();
29227
- this.logger.info("🗑️ Invalidated all admin settings cache");
29978
+ this.logger.info?.("🗑️ Invalidated all admin settings cache");
29228
29979
  }
29229
29980
  /**
29230
29981
  * Get cache statistics for monitoring
@@ -29257,16 +30008,16 @@ var AdminSettingsCache = class AdminSettingsCache {
29257
30008
  * Warm up the cache by fetching all settings
29258
30009
  */
29259
30010
  async warmUp(db) {
29260
- this.logger.info("🔥 Warming up admin settings cache...");
30011
+ this.logger.info?.("🔥 Warming up admin settings cache...");
29261
30012
  await this.getSettingsMap(db);
29262
- this.logger.info("✅ Admin settings cache warmed up");
30013
+ this.logger.info?.("✅ Admin settings cache warmed up");
29263
30014
  }
29264
30015
  /**
29265
30016
  * Graceful shutdown - cleanup timers
29266
30017
  */
29267
30018
  shutdown() {
29268
30019
  this.stopCleanupTimer();
29269
- this.logger.info("🛑 Admin settings cache shutdown complete");
30020
+ this.logger.info?.("🛑 Admin settings cache shutdown complete");
29270
30021
  }
29271
30022
  };
29272
30023
  /** Address of one cached override, shared by the cache and its callers so lookups are consistent. */
@@ -29615,6 +30366,17 @@ const getFileContent = async (fabFile, { storage, logger }) => {
29615
30366
  }
29616
30367
  return content;
29617
30368
  };
30369
+ /**
30370
+ * Content hash for per-lake FabFile dedup (`findByContentHashesInDataLake`). Shared by every
30371
+ * ingest path that needs to hash bytes before creating a FabFile - the Slack attachment path
30372
+ * (raw downloaded buffer) and the URL/link path (`fetchAndParseURL`'s extracted `textContent`) -
30373
+ * so at least the HASHING ITSELF cannot drift between two copies of the same algorithm.
30374
+ *
30375
+ * This does NOT make `contentHash` one hash domain: the two callers feed it different inputs
30376
+ * (raw bytes vs. extracted text), so the same document added once as an attachment and once as a
30377
+ * link produces two different hashes and is not caught as a duplicate by this field.
30378
+ */
30379
+ const computeContentHash = (content) => createHash$1("sha256").update(content).digest("hex");
29618
30380
  /** The next 1-based version number given the existing (possibly absent) version history. */
29619
30381
  const nextVersionNumber = (versions) => {
29620
30382
  if (!versions || versions.length === 0) return 1;
@@ -29825,19 +30587,6 @@ const EDITABLE_IMAGE_KEY_RE = /\.(jpe?g|png|webp|gif)$/i;
29825
30587
  const PREVIEW_CHUNK = 700;
29826
30588
  const CHARS_PER_TOKEN = 3.5;
29827
30589
  /**
29828
- * Chunks per attached file that cosine retrieval feeds to the model. Three starved small embedders: a
29829
- * chunk is the embedding model's context window less a 20% buffer (see SmartChunker), so three chunks
29830
- * is roughly 69k chars on an 8192-token embedder but only 4.3k on a 512-token one, which answers a
29831
- * question about a 200-row table from 43 rows without saying so.
29832
- *
29833
- * 10 is borrowed from rankChunksForFiles' topK default, but note the two caps differ in shape: that
29834
- * one is global across every file in the search, this one is PER FILE, so a multi-file attachment can
29835
- * yield more chunks here. What bounds the payload is the per-file character budget applied to these
29836
- * results (maxChars in processFabFilesServer), not this count - and that budget now derives from the
29837
- * model's input window rather than its output limit; see attachedContentExtractionBudget.
29838
- */
29839
- const COSINE_SEARCH_TOP_K = 10;
29840
- /**
29841
30590
  * How much of one attached file the cosine scan will read, and in what size pages.
29842
30591
  *
29843
30592
  * Module constants rather than admin settings: unlike a data lake, an attachment is one file the
@@ -30249,14 +30998,28 @@ async function fetchAgentConversationHistory(session, questCount, { db }) {
30249
30998
  return acc;
30250
30999
  }, new Array());
30251
31000
  }
30252
- async function fetchAndConvertFabFiles(fabFileIds, { scope }, { db, storage }) {
30253
- const fabFiles = await db.fabfiles.getAccessibleFiles(fabFileIds, scope);
30254
- return await Promise.all(fabFiles.map(async (file) => {
31001
+ /**
31002
+ * Resolves attachment ids to documents, and reports the ones it could NOT resolve. The missing set
31003
+ * is the point: `getAccessibleFiles` applies a permission scope and simply omits what it rejects, so
31004
+ * an id dropped by the scope filter or by a delete/upload race used to leave no trace anywhere - the
31005
+ * turn ran as though the file had never been attached (#2228). Callers report `missingIds` through
31006
+ * the same channel as the per-file notices rather than inferring the drop from a shorter array.
31007
+ */
31008
+ async function fetchAndConvertFabFiles(fabFileIds, { scope, lakeAccess }, { db, storage, logger }) {
31009
+ const fabFiles = await db.fabfiles.getAccessibleFiles(fabFileIds, scope, lakeAccess);
31010
+ const files = await Promise.all(fabFiles.map(async (file) => {
30255
31011
  return {
30256
31012
  ...file,
30257
31013
  userId: file.userId.toString()
30258
31014
  };
30259
31015
  }));
31016
+ const returnedIds = new Set(files.map((file) => String(file.id)));
31017
+ const missingIds = Array.from(new Set(fabFileIds)).filter((id) => !returnedIds.has(String(id)));
31018
+ if (missingIds.length > 0) logger?.warn(`[fetchAndConvertFabFiles] ${missingIds.length} of ${fabFileIds.length} requested file id(s) were not returned by getAccessibleFiles and contribute nothing to this turn: ${missingIds.join(", ")}`);
31019
+ return {
31020
+ files,
31021
+ missingIds
31022
+ };
30260
31023
  }
30261
31024
  async function getCachedSignedUrl(filePath, storage, db) {
30262
31025
  const key = `cachedSignedUrl:${filePath}`;
@@ -30458,7 +31221,7 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
30458
31221
  for (const chunk of usable) {
30459
31222
  const position = scanned;
30460
31223
  scanned++;
30461
- if (head.length < COSINE_SEARCH_TOP_K) head.push({
31224
+ if (head.length < 10) head.push({
30462
31225
  chunkId: chunk.id,
30463
31226
  content: chunk.text,
30464
31227
  score: 0
@@ -30479,9 +31242,9 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
30479
31242
  position
30480
31243
  });
30481
31244
  }
30482
- if (ranked.length > COSINE_SEARCH_TOP_K) {
31245
+ if (ranked.length > 10) {
30483
31246
  ranked.sort(compareRankedChunks);
30484
- ranked.length = COSINE_SEARCH_TOP_K;
31247
+ ranked.length = 10;
30485
31248
  }
30486
31249
  if (!moreExist) break;
30487
31250
  }
@@ -30503,14 +31266,14 @@ const noopResize = async (imageBuffer) => imageBuffer;
30503
31266
  async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, attachedContentTokenBudget, modelInfo, sendStatusUpdate, { logger, storage, db, resizeImageForModel = noopResize }, progressCallback) {
30504
31267
  if (!fabFiles || fabFiles.length === 0) return {
30505
31268
  userMessages: [],
30506
- errorMessages: [],
31269
+ fileNotices: [],
30507
31270
  deliveredFileIds: [],
30508
31271
  fullyDeliveredFileIds: []
30509
31272
  };
30510
31273
  const fileProcessingStartTime = Date.now();
30511
31274
  let systemContent = "";
30512
31275
  const userMessages = [];
30513
- const errorMessages = [];
31276
+ const fileNotices = [];
30514
31277
  const deliveredFileIds = /* @__PURE__ */ new Set();
30515
31278
  const fullyDeliveredFileIds = /* @__PURE__ */ new Set();
30516
31279
  const contextFiles = [];
@@ -30541,11 +31304,25 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30541
31304
  try {
30542
31305
  if (isAudioMimeType(file.mimeType)) {
30543
31306
  logger.warn(`[processFabFilesServer] Skipping audio file ${file.fileName} — audio is not attachable to an LLM.`);
31307
+ fileNotices.push({
31308
+ fabFileId: file.id,
31309
+ fileName: file.fileName,
31310
+ band: "audio",
31311
+ message: `"${noticeFileName(file.fileName)}" is an audio file and was not sent: no model accepts audio as input.`,
31312
+ delivered: false
31313
+ });
30544
31314
  return;
30545
31315
  }
30546
31316
  if (supportsVision && isImageAttachment(file.mimeType)) {
30547
31317
  if (!isImageServeable(file)) {
30548
31318
  logger.warn(`[processFabFilesServer] Skipping image file ${file.fileName} — held pending moderation or blocked (#9776 Q2b).`);
31319
+ fileNotices.push({
31320
+ fabFileId: file.id,
31321
+ fileName: file.fileName,
31322
+ band: "image_not_serveable",
31323
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: it is held pending moderation or has been blocked.`,
31324
+ delivered: false
31325
+ });
30549
31326
  return;
30550
31327
  }
30551
31328
  sendStatusUpdate(`Processing image file ${file.fileName}...`);
@@ -30581,9 +31358,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30581
31358
  const errorMsg = `⚠️ Image "${file.fileName}" (${fileSizeMB.toFixed(1)}MB) is too large for ${backendName}. Max: ${MAX_IMAGE_SIZE_MB}MB. Please delete this file and re-upload to auto-resize.`;
30582
31359
  logger.warn(errorMsg);
30583
31360
  await sendStatusUpdate(errorMsg);
30584
- errorMessages.push({
30585
- role: "error",
30586
- content: errorMsg
31361
+ fileNotices.push({
31362
+ fabFileId: file.id,
31363
+ fileName: file.fileName,
31364
+ band: "image_too_large",
31365
+ message: errorMsg,
31366
+ delivered: false
30587
31367
  });
30588
31368
  return;
30589
31369
  }
@@ -30613,9 +31393,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30613
31393
  const errorMsg = `⚠️ Image "${file.fileName}" (${encodedMB}MB encoded) is too large for ${modelInfo.name}. Max ~3MB. Please delete this file and re-upload a smaller image.`;
30614
31394
  logger.warn(errorMsg);
30615
31395
  await sendStatusUpdate(errorMsg);
30616
- errorMessages.push({
30617
- role: "error",
30618
- content: errorMsg
31396
+ fileNotices.push({
31397
+ fabFileId: file.id,
31398
+ fileName: file.fileName,
31399
+ band: "image_too_large",
31400
+ message: errorMsg,
31401
+ delivered: false
30619
31402
  });
30620
31403
  return;
30621
31404
  }
@@ -30629,7 +31412,16 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30629
31412
  });
30630
31413
  delivered = true;
30631
31414
  fullyDelivered = true;
30632
- } else logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
31415
+ } else {
31416
+ logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
31417
+ fileNotices.push({
31418
+ fabFileId: file.id,
31419
+ fileName: file.fileName,
31420
+ band: "vision_unsupported",
31421
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: image input is not implemented for ${modelInfo.name ?? modelInfo.id}.`,
31422
+ delivered: false
31423
+ });
31424
+ }
30633
31425
  break;
30634
31426
  case ModelBackend.Ollama: {
30635
31427
  const imageBuffer = await resizeImageForModel(await storage.download(file.filePath), void 0, logger);
@@ -30658,10 +31450,26 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30658
31450
  fullyDelivered = true;
30659
31451
  break;
30660
31452
  }
30661
- default: logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
31453
+ default:
31454
+ logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
31455
+ fileNotices.push({
31456
+ fabFileId: file.id,
31457
+ fileName: file.fileName,
31458
+ band: "unsupported_backend",
31459
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: this model's backend does not accept image attachments.`,
31460
+ delivered: false
31461
+ });
30662
31462
  }
30663
- } else if (!supportsVision && isImageAttachment(file.mimeType)) logger.warn(`File ${file.fileName} is an image but model does not support vision. Skipping...`);
30664
- else {
31463
+ } else if (!supportsVision && isImageAttachment(file.mimeType)) {
31464
+ logger.warn(`File ${file.fileName} is an image but model does not support vision. Skipping...`);
31465
+ fileNotices.push({
31466
+ fabFileId: file.id,
31467
+ fileName: file.fileName,
31468
+ band: "vision_unsupported",
31469
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: ${modelInfo?.name ?? modelInfo?.id ?? "this model"} cannot read images.`,
31470
+ delivered: false
31471
+ });
31472
+ } else {
30665
31473
  const embeddingModel = file.embeddingModel ?? OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002;
30666
31474
  const userVector = userVectorPrompt[embeddingModel];
30667
31475
  const canCosineSearch = file.vectorized && !!userVector && userVector.length > 0;
@@ -30737,9 +31545,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30737
31545
  const originalFileSize = fabContent.length;
30738
31546
  fabContent = fabContent.substring(0, finalMaxFileSize ?? PREVIEW_CHUNK) + CONTENT_TRUNCATION_NOTICE;
30739
31547
  errorMsg = `Knowledge in the workbench with the fileName ${file.fileName} is ${originalFileSize} long which exceeds ${finalMaxFileSize}. ` + (canCosineSearch ? "None of its vectorized chunks could be searched with this turn's embedding model, so it was sent as raw text and truncated. Re-vectorize it under the current embedding model, or select a model with a higher context window." : "Vectorize your large file or select a model with higher context window.");
30740
- errorMessages.push({
30741
- role: "error",
30742
- content: errorMsg
31548
+ fileNotices.push({
31549
+ fabFileId: file.id,
31550
+ fileName: file.fileName,
31551
+ band: "truncated",
31552
+ message: `"${noticeFileName(file.fileName)}" was too large to send whole; only the first ${Math.floor(finalMaxFileSize)} characters of ${originalFileSize} reached this conversation.`,
31553
+ delivered: true
30743
31554
  });
30744
31555
  } else errorMsg = null;
30745
31556
  delivered = true;
@@ -30753,19 +31564,41 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30753
31564
  error: errorMsg
30754
31565
  });
30755
31566
  } catch (e) {
30756
- if (e instanceof BadRequestError && e.message.includes("Unsupported file type")) logger.warn(`Unsupported file type: ${file.fileName}`);
30757
- else if (isAxiosError(e) && e.response?.status === 404) {
31567
+ if (e instanceof BadRequestError && e.message.includes("Unsupported file type")) {
31568
+ logger.warn(`Unsupported file type: ${file.fileName}`);
31569
+ fileNotices.push({
31570
+ fabFileId: file.id,
31571
+ fileName: file.fileName,
31572
+ band: "unsupported_type",
31573
+ message: `"${noticeFileName(file.fileName)}" was not sent: its file type (${file.mimeType}) cannot be read as text.`,
31574
+ delivered: false
31575
+ });
31576
+ } else if (isAxiosError(e) && e.response?.status === 404) {
30758
31577
  await sendStatusUpdate(`Skipping file ${file.fileName}. File might be corrupted or deleted`);
30759
31578
  await db.fabfiles.update({
30760
31579
  id: file.id,
30761
31580
  error: "This file appears to be corrupted or may have been deleted. Please try uploading the file again."
30762
31581
  });
31582
+ fileNotices.push({
31583
+ fabFileId: file.id,
31584
+ fileName: file.fileName,
31585
+ band: "read_failed",
31586
+ message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: it appears to be corrupted or deleted. Try uploading it again.`,
31587
+ delivered: false
31588
+ });
30763
31589
  } else if (e instanceof CorruptedFileError) {
30764
31590
  await sendStatusUpdate(`Skipping corrupted file ${file.fileName}. Please try re-uploading`);
30765
31591
  await db.fabfiles.update({
30766
31592
  id: file.id,
30767
31593
  error: e.message
30768
31594
  });
31595
+ fileNotices.push({
31596
+ fabFileId: file.id,
31597
+ fileName: file.fileName,
31598
+ band: "read_failed",
31599
+ message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: ${e.message}`,
31600
+ delivered: false
31601
+ });
30769
31602
  } else {
30770
31603
  logger.updateMetadata({ filePath: file.filePath });
30771
31604
  throw e;
@@ -30785,6 +31618,18 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30785
31618
  processedFiles++;
30786
31619
  if (progressCallback) await progressCallback(processedFiles, totalFiles);
30787
31620
  }))));
31621
+ const noticedFileIds = new Set(fileNotices.map((notice) => notice.fabFileId));
31622
+ for (const file of fabFiles) {
31623
+ if (deliveredFileIds.has(file.id) || noticedFileIds.has(file.id)) continue;
31624
+ logger.warn(`[processFabFilesServer] "${file.fileName}" (${file.id}) contributed no content and produced no notice; reporting it as undelivered.`);
31625
+ fileNotices.push({
31626
+ fabFileId: file.id,
31627
+ fileName: file.fileName,
31628
+ band: "no_readable_content",
31629
+ message: `"${noticeFileName(file.fileName)}" was not sent: no readable content could be extracted from it.`,
31630
+ delivered: false
31631
+ });
31632
+ }
30788
31633
  if (imageContent.length > 0) userMessages.push({
30789
31634
  role: "user",
30790
31635
  content: imageContent
@@ -30812,7 +31657,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
30812
31657
  logger.info(`📁 File processing completed in ${fileProcessingTime}ms for ${fabFiles.length} files`);
30813
31658
  return {
30814
31659
  userMessages,
30815
- errorMessages,
31660
+ fileNotices,
30816
31661
  deliveredFileIds: Array.from(deliveredFileIds),
30817
31662
  fullyDeliveredFileIds: Array.from(fullyDeliveredFileIds)
30818
31663
  };
@@ -31337,6 +32182,7 @@ var llm_exports = /* @__PURE__ */ __exportAll({
31337
32182
  ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
31338
32183
  ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
31339
32184
  BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
32185
+ COSINE_SEARCH_TOP_K: () => 10,
31340
32186
  DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
31341
32187
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
31342
32188
  FORMAT_PROMPT_PRIORITY: () => 60,
@@ -32955,6 +33801,7 @@ const OPENAI_IMAGE_CLIENT_OPTS = {
32955
33801
  maxRetries: 0
32956
33802
  };
32957
33803
  const ALTERNATIVE_IMAGE_MODELS = "Flux Pro, Flux Dev, or Grok";
33804
+ const truncatePromptForLog = (prompt) => prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
32958
33805
  /**
32959
33806
  * Builds a user-friendly error when OpenAI's safety system blocks an image
32960
33807
  * request, guiding the user to rephrase or switch to an alternative model.
@@ -32982,6 +33829,53 @@ function buildModerationBlockedError(error) {
32982
33829
 
32983
33830
  Tip: Switch to an alternative model with different content policies — e.g. ${ALTERNATIVE_IMAGE_MODELS} — which may accept this prompt.\n\nIf you believe this is an error, you can report it to OpenAI with request ID: ${requestId}`);
32984
33831
  }
33832
+ /**
33833
+ * Splits a WIDTHxHEIGHT size into its two edges, or null when the value is not a
33834
+ * pair of non-zero numbers (e.g. 'auto', '', 'wide'). Null means "not a custom
33835
+ * resolution" rather than "invalid": generate() has always left such values
33836
+ * untouched, and that behaviour is preserved.
33837
+ */
33838
+ function parseSizeEdges(size) {
33839
+ if (typeof size !== "string") return null;
33840
+ const [width, height] = size.split("x").map(Number);
33841
+ if (!width || !height) return null;
33842
+ return {
33843
+ width,
33844
+ height
33845
+ };
33846
+ }
33847
+ /**
33848
+ * True when a custom gpt-image-2 resolution meets OpenAI's documented limits.
33849
+ * gpt-image-2 accepts any resolution satisfying these, not only the presets in
33850
+ * OPENAI_GPT_IMAGE_2_IMAGE_SIZES, so a flat preset check would reject valid
33851
+ * custom sizes. Must stay the single source of this rule for generate() and edit().
33852
+ */
33853
+ function satisfiesGptImage2Constraints({ width, height }) {
33854
+ const { maxEdge, minTotalPixels, maxTotalPixels, edgeMultiple, maxAspectRatio } = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.constraints;
33855
+ const longEdge = Math.max(width, height);
33856
+ const shortEdge = Math.min(width, height);
33857
+ const totalPixels = width * height;
33858
+ return longEdge <= maxEdge && width % edgeMultiple === 0 && height % edgeMultiple === 0 && longEdge / shortEdge <= maxAspectRatio && totalPixels >= minTotalPixels && totalPixels <= maxTotalPixels;
33859
+ }
33860
+ /**
33861
+ * True when `size` may be forwarded to images.edit for `model`. gpt-image-2 takes
33862
+ * its presets (including 'auto') or any custom WIDTHxHEIGHT meeting the same
33863
+ * constraints generate() enforces; the gpt-image-1 family is limited to its three
33864
+ * fixed sizes. An unsupported size is dropped by the caller so OpenAI applies its
33865
+ * own default instead of rejecting the whole request with a 400.
33866
+ *
33867
+ * GPT-Image tiers only: dall-e-2 has its own size list and passes size through
33868
+ * untouched, so do not route that model here.
33869
+ */
33870
+ function isSupportedEditSize(model, size) {
33871
+ if (typeof size !== "string") return false;
33872
+ if (isGPTImage2Model(model)) {
33873
+ if (OPENAI_GPT_IMAGE_2_IMAGE_SIZES.includes(size)) return true;
33874
+ const edges = parseSizeEdges(size);
33875
+ return edges !== null && satisfiesGptImage2Constraints(edges);
33876
+ }
33877
+ return OPENAI_GPT_IMAGE_1_IMAGE_SIZES.includes(size);
33878
+ }
32985
33879
  var OpenAIImageService = class extends AIImageService {
32986
33880
  async generate(prompt, options) {
32987
33881
  const openai = new OpenAI({
@@ -33006,16 +33900,11 @@ var OpenAIImageService = class extends AIImageService {
33006
33900
  }
33007
33901
  if (isGPTImage2Model(options.model)) {
33008
33902
  if (openaiOptions.size && openaiOptions.size !== "auto") {
33009
- const [w, h] = openaiOptions.size.split("x").map(Number);
33010
- if (w && h) {
33011
- const maxEdge = Math.max(w, h);
33012
- const minEdge = Math.min(w, h);
33013
- const totalPixels = w * h;
33014
- if (maxEdge > 3840 || w % 16 !== 0 || h % 16 !== 0 || maxEdge / minEdge > 3 || totalPixels < 655360 || totalPixels > 8294400) {
33015
- const originalSize = openaiOptions.size;
33016
- openaiOptions.size = "1024x1024";
33017
- parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
33018
- }
33903
+ const edges = parseSizeEdges(openaiOptions.size);
33904
+ if (edges && !satisfiesGptImage2Constraints(edges)) {
33905
+ const originalSize = openaiOptions.size;
33906
+ openaiOptions.size = "1024x1024";
33907
+ parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
33019
33908
  }
33020
33909
  } else if (!openaiOptions.size) openaiOptions.size = "auto";
33021
33910
  } else {
@@ -33068,6 +33957,10 @@ var OpenAIImageService = class extends AIImageService {
33068
33957
  const imageFile = new File([pngBuffer], "image.png", { type: "image/png" });
33069
33958
  if (isGPTImageModel(options.model)) {
33070
33959
  const editModel = options.model || ImageModels.GPT_IMAGE_2;
33960
+ this.logger.log("OpenAI image generation request (edit endpoint, image-to-image):", {
33961
+ model: editModel,
33962
+ prompt: truncatePromptForLog(prompt)
33963
+ });
33071
33964
  result = await openai.images.edit({
33072
33965
  model: editModel,
33073
33966
  image: [imageFile],
@@ -33075,20 +33968,31 @@ var OpenAIImageService = class extends AIImageService {
33075
33968
  });
33076
33969
  } else {
33077
33970
  const { style, quality, model, ...opts } = openaiOptions;
33971
+ const variationSize = [
33972
+ "256x256",
33973
+ "512x512",
33974
+ "1024x1024"
33975
+ ].find((s) => s === openaiOptions.size);
33976
+ this.logger.log("OpenAI image generation request (variation endpoint):", {
33977
+ ...opts,
33978
+ size: variationSize
33979
+ });
33078
33980
  result = await openai.images.createVariation({
33079
33981
  ...opts,
33080
33982
  image: imageFile,
33081
- size: [
33082
- "256x256",
33083
- "512x512",
33084
- "1024x1024"
33085
- ].find((s) => s === openaiOptions.size)
33983
+ size: variationSize
33086
33984
  });
33087
33985
  }
33088
- } else result = await openai.images.generate({
33089
- prompt,
33090
- ...openaiOptions
33091
- });
33986
+ } else {
33987
+ this.logger.log("OpenAI image generation request:", {
33988
+ prompt: truncatePromptForLog(prompt),
33989
+ ...openaiOptions
33990
+ });
33991
+ result = await openai.images.generate({
33992
+ prompt,
33993
+ ...openaiOptions
33994
+ });
33995
+ }
33092
33996
  images = this.imageResponseToUrl(result);
33093
33997
  return images;
33094
33998
  } catch (error) {
@@ -33145,10 +34049,21 @@ var OpenAIImageService = class extends AIImageService {
33145
34049
  Logger.globalInstance.debug(`[DEBUG] ⚠️ Edit endpoint doesn't support ${model}, defaulting to gpt-image-2`);
33146
34050
  editModel = ImageModels.GPT_IMAGE_2;
33147
34051
  }
34052
+ const forwardSize = isSupportedEditSize(editModel, size);
34053
+ this.logger.log("OpenAI image edit request:", {
34054
+ model: editModel,
34055
+ prompt: truncatePromptForLog(prompt),
34056
+ hasMask: !!maskFile,
34057
+ n,
34058
+ size,
34059
+ response_format
34060
+ });
33148
34061
  const response = await openai.images.edit(isGPTImageModel(editModel) ? {
33149
34062
  model: editModel,
33150
34063
  image: [imageFile],
33151
- prompt
34064
+ prompt,
34065
+ ...forwardSize ? { size } : {},
34066
+ ...maskFile ? { mask: maskFile } : {}
33152
34067
  } : {
33153
34068
  model: editModel,
33154
34069
  image: imageFile,
@@ -35175,6 +36090,21 @@ var TiktokenTokenizer = class {
35175
36090
  return Array.from(encoder.encode_ordinary(text));
35176
36091
  }
35177
36092
  /**
36093
+ * Decode token ids back to text through the same encoder encodeTokens used, so an
36094
+ * encode -> slice -> decode round trip yields real text rather than the ids themselves.
36095
+ * @param tokens - Token ids, typically a slice of an encodeTokens result
36096
+ * @param modelId - Model ID to determine encoding (must match the one used to encode)
36097
+ * @returns Promise<string> - The decoded text
36098
+ *
36099
+ * tiktoken's wasm decode() hands back raw UTF-8 bytes. A slice that ends mid-character therefore
36100
+ * decodes to a trailing U+FFFD; callers that sliced are expected to trim it.
36101
+ */
36102
+ async decodeTokens(tokens, modelId, logger) {
36103
+ if (this.isShuttingDown) throw new Error("TiktokenTokenizer is shutting down");
36104
+ const encoder = await this.getEncoder(modelId, logger);
36105
+ return new TextDecoder().decode(encoder.decode(new Uint32Array(tokens)));
36106
+ }
36107
+ /**
35178
36108
  * Returns a lightweight ITokenizer proxy that delegates WASM encoder operations
35179
36109
  * to this instance (preserving the shared encoder cache) but routes log output
35180
36110
  * through the provided logger. Useful for attaching per-request context (e.g.
@@ -35183,7 +36113,8 @@ var TiktokenTokenizer = class {
35183
36113
  withLogger(logger) {
35184
36114
  return {
35185
36115
  countTokens: (text, modelId) => this.countTokens(text, modelId, logger),
35186
- encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger)
36116
+ encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger),
36117
+ decodeTokens: (tokens, modelId) => this.decodeTokens(tokens, modelId, logger)
35187
36118
  };
35188
36119
  }
35189
36120
  /**
@@ -35643,6 +36574,7 @@ __reExport(/* @__PURE__ */ __exportAll({
35643
36574
  BaseStorage: () => BaseStorage,
35644
36575
  BedrockEmbeddingService: () => BedrockEmbeddingService,
35645
36576
  CONTENT_TYPE_BY_FORMAT: () => CONTENT_TYPE_BY_FORMAT,
36577
+ COSINE_SEARCH_TOP_K: () => 10,
35646
36578
  CacheKeys: () => CacheKeys,
35647
36579
  ChunkSchema: () => ChunkSchema,
35648
36580
  CircuitBreaker: () => CircuitBreaker,
@@ -35737,6 +36669,7 @@ __reExport(/* @__PURE__ */ __exportAll({
35737
36669
  checkStorageLimit: () => checkStorageLimit,
35738
36670
  checkStorageLimitForFile: () => checkStorageLimitForFile,
35739
36671
  cleanMermaidSyntax: () => cleanMermaidSyntax,
36672
+ computeContentHash: () => computeContentHash,
35740
36673
  computeCosineSimilarity: () => computeCosineSimilarity,
35741
36674
  computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
35742
36675
  convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
@@ -35808,6 +36741,7 @@ __reExport(/* @__PURE__ */ __exportAll({
35808
36741
  registerLambdaErrorHandlers: () => registerLambdaErrorHandlers,
35809
36742
  registerProcessErrorHandlers: () => registerProcessErrorHandlers,
35810
36743
  registrableDomain: () => registrableDomain,
36744
+ reservationOutputTokens: () => reservationOutputTokens,
35811
36745
  resolveEmbeddingConfig: () => resolveEmbeddingConfig,
35812
36746
  resolveSupportedMimeType: () => resolveSupportedMimeType,
35813
36747
  safeInputWindow: () => safeInputWindow,