@bike4mind/cli 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as PermissionDeniedError, $t as extractSnippetMeta, A as FriendshipEvents, At as XAI_IMAGE_MODELS, B as InboxEvents, Bt as isZodError, C as CorruptedFileError, Ct as TransferCreditTransaction, D as FeedbackEvents, Dt as VIDEO_SIZE_CONSTRAINTS, E as FavoriteDocumentType, Et as UnprocessableEntityError, F as HTTPError, Ft as getViewById, G as LLMEvents, Gt as sanitizeTelemetryError, H as InviteEvents, Ht as obfuscateApiKey, I as HttpStatus, It as isGPTImage2Model, J as ModelBackend, Jt as substituteArguments$1, K as MiscEvents, Kt as secureParameters, L as ImageEditUsageTransaction, Lt as isGPTImageModel, M as GenerateImageToolCallSchema, Mt as dayjsConfig_default, N as GenericCreditAddTransaction, Nt as getAccessibleDataLakes, O as FileEvents, Ot as VideoGenerationUsageTransaction, P as GenericCreditDeductTransaction, Pt as getMcpProviderMetadata, Q as Permission, Qt as buildRateLimitLogEntry, R as ImageGenerationUsageTransaction, Rt as isModelAccessible, S as CompletionApiUsageTransaction, St as ToolUsageTransaction, T as ElabsEvents, Tt as UnauthorizedError, U as InviteType, Ut as parseSkillArguments, V as InternalServerError, Vt as mapMimeTypeToArtifactType, W as KnowledgeType, Wt as resolveNavigationIntents, X as OpenAIEmbeddingModel, Y as NotFoundError, Yt as toDataLakeConfig, Z as OpenAIImageGenerationInput, _ as BadRequestError, _t as SupportedFabFileMimeTypes, at as QuestMasterParamsSchema, b as ChatModels, bt as TextGenerationUsageTransaction, c as AiEvents, ct as RechartsChartTypeList, d as ApiKeyType, dt as ResearchTaskExecutionType, en as isNearLimit, et as ProfileEvents, f as AppFileEvents, ft as ResearchTaskPeriodicFrequencyType, g as BFL_SAFETY_TOLERANCE, gt as SubscriptionCreditTransaction, h as BFL_IMAGE_MODELS, ht as SpeechToTextUsageTransaction, it as PurchaseTransaction, j as GEMINI_IMAGE_MODELS, jt as b4mLLMTools, k as ForbiddenError, kt as VideoModels, l as ApiKeyEvents, lt as RegInviteEvents, m as AuthEvents, mt as SessionEvents, n as logger, nn as CollectionType, nt as PromptIntentSchema, ot as RealtimeVoiceUsageTransaction, p as ArtifactTypeSchema, pt as ResearchTaskType, q as ModalEvents, qt as settingsMap, rt as PromptMetaZodSchema, s as ALERT_THRESHOLDS, st as ReceivedCreditTransaction, t as ConfigStore, tn as parseRateLimitHeaders, tt as ProjectEvents, u as ApiKeyScope, ut as ResearchModeParamsSchema, vt as TagType, w as DashboardParamsSchema, wt as UiNavigationEvents, x as ClaudeArtifactMimeTypes, xt as TooManyRequestsError, y as ChatCompletionCreateInputSchema, yt as TaskScheduleHandler, z as ImageModels, zt as isSupportedEmbeddingModel } from "./ConfigStore-BbTg-UaE.mjs";
2
+ import { $ as PermissionDeniedError, $t as wrapUntrustedSkillBody, A as FriendshipEvents, At as XAI_IMAGE_MODELS, B as InboxEvents, Bt as isSupportedEmbeddingModel, C as CorruptedFileError, Ct as TransferCreditTransaction, D as FeedbackEvents, Dt as VIDEO_SIZE_CONSTRAINTS, E as FavoriteDocumentType, Et as UnprocessableEntityError, F as HTTPError, Ft as getViewById, G as LLMEvents, Gt as resolveNavigationIntents, H as InviteEvents, Ht as mapMimeTypeToArtifactType, I as HttpStatus, It as intersectAllowedTools, J as ModelBackend, Jt as settingsMap, K as MiscEvents, Kt as sanitizeTelemetryError, L as ImageEditUsageTransaction, Lt as isGPTImage2Model, M as GenerateImageToolCallSchema, Mt as dayjsConfig_default, N as GenericCreditAddTransaction, Nt as getAccessibleDataLakes, O as FileEvents, Ot as VideoGenerationUsageTransaction, P as GenericCreditDeductTransaction, Pt as getMcpProviderMetadata, Q as Permission, R as ImageGenerationUsageTransaction, Rt as isGPTImageModel, S as CompletionApiUsageTransaction, St as ToolUsageTransaction, T as ElabsEvents, Tt as UnauthorizedError, U as InviteType, Ut as obfuscateApiKey, V as InternalServerError, Vt as isZodError, W as KnowledgeType, Wt as parseSkillArguments, X as OpenAIEmbeddingModel, Xt as toDataLakeConfig, Y as NotFoundError, Yt as substituteArguments$1, Z as OpenAIImageGenerationInput, _ as BadRequestError, _t as SupportedFabFileMimeTypes, at as QuestMasterParamsSchema, b as ChatModels, bt as TextGenerationUsageTransaction, c as AiEvents, ct as RechartsChartTypeList, d as ApiKeyType, dt as ResearchTaskExecutionType, en as buildRateLimitLogEntry, et as ProfileEvents, f as AppFileEvents, ft as ResearchTaskPeriodicFrequencyType, g as BFL_SAFETY_TOLERANCE, gt as SubscriptionCreditTransaction, h as BFL_IMAGE_MODELS, ht as SpeechToTextUsageTransaction, in as CollectionType, it as PurchaseTransaction, j as GEMINI_IMAGE_MODELS, jt as b4mLLMTools, k as ForbiddenError, kt as VideoModels, l as ApiKeyEvents, lt as RegInviteEvents, m as AuthEvents, mt as SessionEvents, n as logger, nn as isNearLimit, nt as PromptIntentSchema, ot as RealtimeVoiceUsageTransaction, p as ArtifactTypeSchema, pt as ResearchTaskType, q as ModalEvents, qt as secureParameters, rn as parseRateLimitHeaders, rt as PromptMetaZodSchema, s as ALERT_THRESHOLDS, st as ReceivedCreditTransaction, t as ConfigStore, tn as extractSnippetMeta, tt as ProjectEvents, u as ApiKeyScope, ut as ResearchModeParamsSchema, vt as TagType, w as DashboardParamsSchema, wt as UiNavigationEvents, x as ClaudeArtifactMimeTypes, xt as TooManyRequestsError, y as ChatCompletionCreateInputSchema, yt as TaskScheduleHandler, z as ImageModels, zt as isModelAccessible } from "./ConfigStore-DL7p3ZiY.mjs";
3
3
  import { a as isUserLockedOut, c as userCanDisableMFA, d as userRequiresMFA, f as verifyBackupCode, i as getLockoutTimeRemaining, l as userEligibleForMFA, n as generateBackupCodes, o as recordFailedAttempt, p as verifyTOTPToken, r as generateTOTPSetup, s as shouldResetFailedAttempts, t as clearFailedAttempts, u as userHasMFAConfigured } from "./utils-fqhJmuB8.mjs";
4
4
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
5
- import { t as version } from "./package-CSa_ad8r.mjs";
5
+ import { t as version } from "./package-B-ONmxJ7.mjs";
6
6
  import { execFile, execFileSync, spawn } from "child_process";
7
7
  import crypto, { createHash, randomBytes, randomUUID } from "crypto";
8
8
  import { existsSync, promises, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -28,7 +28,7 @@ import { Logger, Logger as Logger$1 } from "@bike4mind/observability";
28
28
  import { BEDROCK_EMBEDDING_MODEL_MAP, BaseStorage, BaseStorage as BaseStorage$1, BedrockEmbeddingService, ChunkSchema, EmbeddingFactory, EmbeddingModelProvider, EmbeddingService, OPENAI_EMBEDDING_MODEL_MAP, OpenAIEmbeddingService, S3Storage, SmartChunker, URL_REGEX, VOYAGEAI_EMBEDDING_MODEL_MAP, VoyageAIEmbeddingProvider, detectURLs, detectURLs as detectURLs$1, fetchAndParseURL, fetchAndParseURL as fetchAndParseURL$1, getProviderFromModel, hasURLs, hasURLs as hasURLs$1, isPrivateIP, isPrivateOrInternalHostname, urlExists, validateUrlForFetch } from "@bike4mind/fab-pipeline";
29
29
  import * as dotenv from "dotenv";
30
30
  import * as import__bike4mind_llm_adapters from "@bike4mind/llm-adapters";
31
- import { OpenAIBackend, ensureToolPairingIntegrity } from "@bike4mind/llm-adapters";
31
+ import { OpenAIBackend, ensureToolPairingIntegrity, toProviderEndUserId } from "@bike4mind/llm-adapters";
32
32
  import { fileTypeFromBuffer } from "file-type";
33
33
  import invert from "lodash/invert.js";
34
34
  import * as util from "node:util";
@@ -7086,7 +7086,9 @@ function shouldTriggerFallback(error) {
7086
7086
  "api key",
7087
7087
  "quota exceeded",
7088
7088
  "model not found",
7089
- "does not exist"
7089
+ "does not exist",
7090
+ "is not available",
7091
+ "safety classifier refusal"
7090
7092
  ].some((trigger) => message.includes(trigger));
7091
7093
  }
7092
7094
  /**
@@ -7149,17 +7151,20 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7149
7151
  "claude-opus-4-8",
7150
7152
  "claude-opus-4-7",
7151
7153
  "claude-opus-4-6",
7154
+ "claude-sonnet-5",
7152
7155
  "claude-sonnet-4-6",
7153
7156
  "gpt-5"
7154
7157
  ],
7155
7158
  "claude-opus-4-8": [
7156
7159
  "claude-opus-4-7",
7157
7160
  "claude-opus-4-6",
7161
+ "claude-sonnet-5",
7158
7162
  "claude-sonnet-4-6",
7159
7163
  "gpt-5"
7160
7164
  ],
7161
7165
  "claude-opus-4-7": [
7162
7166
  "claude-opus-4-6",
7167
+ "claude-sonnet-5",
7163
7168
  "claude-sonnet-4-6",
7164
7169
  "gpt-5"
7165
7170
  ],
@@ -7170,17 +7175,26 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7170
7175
  "claude-haiku-4-5-20251001"
7171
7176
  ],
7172
7177
  "claude-opus-4-6": [
7178
+ "claude-sonnet-5",
7173
7179
  "claude-sonnet-4-6",
7174
7180
  "claude-sonnet-4-5-20250929",
7175
7181
  "gpt-5",
7176
7182
  "claude-haiku-4-5-20251001"
7177
7183
  ],
7184
+ "claude-sonnet-5": [
7185
+ "claude-sonnet-4-6",
7186
+ "claude-sonnet-4-5-20250929",
7187
+ "gpt-5"
7188
+ ],
7189
+ "global.anthropic.claude-sonnet-5": ["global.anthropic.claude-sonnet-4-6", "gpt-5"],
7178
7190
  "claude-sonnet-4-6": [
7191
+ "claude-sonnet-5",
7179
7192
  "claude-sonnet-4-5-20250929",
7180
7193
  "claude-opus-4-5-20251101",
7181
7194
  "gpt-5"
7182
7195
  ],
7183
7196
  "claude-sonnet-4-5-20250929": [
7197
+ "claude-sonnet-5",
7184
7198
  "claude-sonnet-4-6",
7185
7199
  "claude-haiku-4-5-20251001",
7186
7200
  "gpt-5"
@@ -7212,7 +7226,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7212
7226
  ],
7213
7227
  "gpt-4o-mini": ["claude-haiku-4-5-20251001", "gpt-3.5-turbo"]
7214
7228
  }[originalModel.id] || [];
7215
- if (preferences.length === 0) preferences.push("claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "gpt-5", "claude-haiku-4-5-20251001");
7229
+ if (preferences.length === 0) preferences.push("claude-sonnet-5", "claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "gpt-5", "claude-haiku-4-5-20251001");
7216
7230
  for (const modelId of preferences) {
7217
7231
  const fallbackModel = availableModels.find((m) => m.id === modelId);
7218
7232
  if (fallbackModel) {
@@ -7869,7 +7883,7 @@ function classifyError(error) {
7869
7883
  return "unhandled_rejection";
7870
7884
  }
7871
7885
  /**
7872
- * Builds a structured log entry for Lambda error handlers.
7886
+ * Builds a structured log entry for the global error handlers.
7873
7887
  */
7874
7888
  function buildLogEntry(error, category) {
7875
7889
  return {
@@ -7884,29 +7898,33 @@ function buildLogEntry(error, category) {
7884
7898
  /**
7885
7899
  * Registers global handlers for unhandled promise rejections and uncaught exceptions.
7886
7900
  *
7887
- * IMPORTANT: This is for OBSERVABILITY ONLY. AWS Lambda's default handlers will still
7888
- * cause the invocation to fail. These handlers allow us to:
7889
- * 1. Log errors with structured context before Lambda exits
7901
+ * Provider-agnostic core shared by Lambda handlers and the always-on
7902
+ * QuestProcessorService container. The handlers:
7903
+ * 1. Log errors with structured context
7890
7904
  * 2. Classify error types for easier debugging
7891
- * 3. Distinguish network errors from application errors
7905
+ * 3. Distinguish network errors from application errors and swallow the network
7906
+ * class (mark the rejection handled / don't re-throw) so a single transient
7907
+ * network failure doesn't escalate
7892
7908
  *
7893
- * DO NOT call process.exit() - let Lambda manage the lifecycle.
7909
+ * CRITICAL for long-running processes: these handlers NEVER call process.exit().
7910
+ * On AWS Lambda that lets the runtime own the lifecycle. On a long-running Fargate
7911
+ * task it means one quest's mid-stream EPIPE / orphaned undici rejection (#8055,
7912
+ * #8197) is logged and swallowed instead of taking Node's default
7913
+ * `uncaughtException` path, which would terminate the whole container and kill
7914
+ * every other in-flight quest.
7894
7915
  *
7895
7916
  * @param logger - Optional logger instance. Falls back to console if not provided.
7917
+ * @param context - Short label used in log message prefixes (default 'Process').
7896
7918
  *
7897
7919
  * @example
7898
7920
  * ```typescript
7899
- * import { registerLambdaErrorHandlers } from '@bike4mind/utils';
7921
+ * import { registerProcessErrorHandlers } from '@bike4mind/utils';
7900
7922
  *
7901
- * // Call at the top of your Lambda handler file (after imports)
7902
- * registerLambdaErrorHandlers();
7903
- *
7904
- * export const handler = async (event) => {
7905
- * // Your handler code
7906
- * };
7923
+ * // Call once at startup, after imports
7924
+ * registerProcessErrorHandlers(logger, 'QuestProcessorService');
7907
7925
  * ```
7908
7926
  */
7909
- function registerLambdaErrorHandlers(logger) {
7927
+ function registerProcessErrorHandlers(logger, context = "Process") {
7910
7928
  if (isRegistered) return;
7911
7929
  isRegistered = true;
7912
7930
  const log = logger || console;
@@ -7915,17 +7933,27 @@ function registerLambdaErrorHandlers(logger) {
7915
7933
  const isNetworkError = category.startsWith("network_");
7916
7934
  const logEntry = buildLogEntry(reason, category);
7917
7935
  if (isNetworkError) {
7918
- log.warn("[Lambda] Network error (unhandled rejection)", logEntry);
7936
+ log.warn(`[${context}] Network error (unhandled rejection)`, logEntry);
7919
7937
  promise.catch(() => {});
7920
- } else log.error("[Lambda] Unhandled promise rejection", logEntry);
7938
+ } else log.error(`[${context}] Unhandled promise rejection`, logEntry);
7921
7939
  });
7922
7940
  process.on("uncaughtException", (error) => {
7923
7941
  const category = classifyError(error);
7924
- if (category.startsWith("network_")) log.warn("[Lambda] Network error (uncaught exception)", buildLogEntry(error, category));
7925
- else log.error("[Lambda] Uncaught exception", buildLogEntry(error, "uncaught_exception"));
7942
+ if (category.startsWith("network_")) log.warn(`[${context}] Network error (uncaught exception)`, buildLogEntry(error, category));
7943
+ else log.error(`[${context}] Uncaught exception`, buildLogEntry(error, "uncaught_exception"));
7926
7944
  });
7927
7945
  }
7928
7946
  /**
7947
+ * Back-compat alias for Lambda handlers. Preserves the historical `[Lambda]` log
7948
+ * prefix so existing log-based filters/alerts are unaffected. New code on
7949
+ * non-Lambda runtimes should call {@link registerProcessErrorHandlers} directly.
7950
+ *
7951
+ * @param logger - Optional logger instance. Falls back to console if not provided.
7952
+ */
7953
+ function registerLambdaErrorHandlers(logger) {
7954
+ registerProcessErrorHandlers(logger, "Lambda");
7955
+ }
7956
+ /**
7929
7957
  * Reset the registration state. Only for testing purposes.
7930
7958
  * @internal
7931
7959
  */
@@ -8055,6 +8083,7 @@ __reExport(/* @__PURE__ */ __exportAll$1({
8055
8083
  processFabFilesServer: () => processFabFilesServer,
8056
8084
  processUrlsFromPrompt: () => processUrlsFromPrompt,
8057
8085
  registerLambdaErrorHandlers: () => registerLambdaErrorHandlers,
8086
+ registerProcessErrorHandlers: () => registerProcessErrorHandlers,
8058
8087
  secureParameters: () => secureParameters,
8059
8088
  sendToConnection: () => sendToConnection,
8060
8089
  shouldTriggerFallback: () => shouldTriggerFallback,
@@ -10738,11 +10767,12 @@ RULES:
10738
10767
  5. Each machine can only process one operation at a time
10739
10768
  6. Assign reasonable durations if not specified (use whole numbers)
10740
10769
  7. Create meaningful names for jobs and machines based on context
10770
+ 8. "description" MUST be a single plain-English sentence summarizing the real-world scenario this models (what the user just described) — it is shown on the on-screen brief card so a human can recognize at a glance that it matches the conversation. e.g. "Scheduling the 6 high-priority sprint issues across 5 developers to minimize total completion time."
10741
10771
 
10742
10772
  RESPOND WITH ONLY A JSON OBJECT matching this schema:
10743
10773
  {
10744
- "name": "string - descriptive problem name",
10745
- "description": "string - brief description",
10774
+ "name": "string - short descriptive problem name",
10775
+ "description": "string - one plain-English sentence summarizing the scenario (see rule 8)",
10746
10776
  "jobs": [
10747
10777
  {
10748
10778
  "id": 0,
@@ -13491,7 +13521,7 @@ const FAMILY_SAMPLE_PROBLEMS = {
13491
13521
  continuous: continuousSampleProblems
13492
13522
  };
13493
13523
  //#endregion
13494
- //#region ../../b4m-core/services/dist/tools-BhiSgg5-.mjs
13524
+ //#region ../../b4m-core/services/dist/tools-BIrr-Tc6.mjs
13495
13525
  const createFabFileSchema = z.object({
13496
13526
  fileName: z.string(),
13497
13527
  mimeType: z.string(),
@@ -13683,7 +13713,7 @@ async function performDeepResearch(context, params, config = {}) {
13683
13713
  let analysisLlm = context.llm;
13684
13714
  let analysisModel = config.model || ChatModels.GPT4_1;
13685
13715
  if (config.apiKeys?.openai) try {
13686
- analysisLlm = new OpenAIBackend(config.apiKeys.openai, context.logger);
13716
+ analysisLlm = new OpenAIBackend(config.apiKeys.openai, context.logger, toProviderEndUserId(context.userId));
13687
13717
  analysisModel = ChatModels.GPT4_1;
13688
13718
  log("🔬 Deep Research: Using GPT-4.1 for analysis");
13689
13719
  } catch (error) {
@@ -13919,18 +13949,21 @@ async function getDynamicDataLakeAccess(context) {
13919
13949
  const userTags = context.user.tags || [];
13920
13950
  const entitlementKeys = context.entitlementKeys ?? [];
13921
13951
  const organizationId = context.user.organizationId ? String(context.user.organizationId) : void 0;
13952
+ const userId = context.user.id ? String(context.user.id) : void 0;
13922
13953
  let dynamicDataLakes;
13923
13954
  if (context.db.dataLakes) try {
13924
- dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys, organizationId)).map(toDataLakeConfig);
13955
+ dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys, organizationId, userId)).map(toDataLakeConfig);
13925
13956
  } catch {}
13926
13957
  const accessibleLakes = getAccessibleDataLakes(userTags, dynamicDataLakes, entitlementKeys);
13958
+ const dynamicIds = new Set((dynamicDataLakes ?? []).map((d) => d.id));
13927
13959
  return {
13928
13960
  dataLakeTags: accessibleLakes.map((dl) => dl.datalakeTag),
13929
- dataLakeTagPrefixes: accessibleLakes.map((dl) => dl.fileTagPrefix)
13961
+ dataLakeTagPrefixes: accessibleLakes.filter((dl) => !dynamicIds.has(dl.id)).map((dl) => dl.fileTagPrefix),
13962
+ scopedTagPrefixes: accessibleLakes.filter((dl) => dynamicIds.has(dl.id)).map((dl) => dl.fileTagPrefix)
13930
13963
  };
13931
13964
  }
13932
13965
  async function semanticDataLakeSearch(params, adapters) {
13933
- const { userId, userGroups = [], query, tags = [], topK = 10, minScore = 0, embeddingModel, apiKeyTable, dataLakeTags, dataLakeTagPrefixes, maxFiles = 2e3, chunkLoadCap = 1e4, logger } = params;
13966
+ const { userId, userGroups = [], query, tags = [], topK = 10, minScore = 0, embeddingModel, apiKeyTable, dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes = [], maxFiles = 2e3, chunkLoadCap = 1e4, logger } = params;
13934
13967
  const empty = {
13935
13968
  results: [],
13936
13969
  totalChunksSearched: 0,
@@ -13964,6 +13997,7 @@ async function semanticDataLakeSearch(params, adapters) {
13964
13997
  userGroups,
13965
13998
  dataLakeTags,
13966
13999
  dataLakeTagPrefixes,
14000
+ scopedTagPrefixes,
13967
14001
  excludeContent: true
13968
14002
  });
13969
14003
  const fileIds = fileSearch.data.map((f) => f.id);
@@ -14876,7 +14910,7 @@ const currentDateTimeTool = {
14876
14910
  },
14877
14911
  toolSchema: {
14878
14912
  name: "current_datetime",
14879
- description: "Get the current date and time with advanced features. Supports multiple output formats, date calculations (days until/since), historical day lookups, and IANA timezone identifiers.",
14913
+ description: "Get the current, real-world date and time, fresh at the moment of the call. Call this whenever you need the current time of day (hour/minute) or to timestamp an action as it executes — never guess or rely on memory. Supports multiple output formats, date calculations (days until/since), historical day lookups, and IANA timezone identifiers.",
14880
14914
  parameters: {
14881
14915
  type: "object",
14882
14916
  properties: {
@@ -17295,7 +17329,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
17295
17329
  const provider = getProviderFromModel(embeddingModel);
17296
17330
  if (provider === "openai" && !apiKeyTable?.openai) return null;
17297
17331
  if (provider === "voyageai" && !apiKeyTable?.voyageai) return null;
17298
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17332
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
17299
17333
  if (dataLakeTags.length === 0) return null;
17300
17334
  const search = await semanticDataLakeSearch({
17301
17335
  userId: context.userId,
@@ -17308,6 +17342,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
17308
17342
  apiKeyTable,
17309
17343
  dataLakeTags,
17310
17344
  dataLakeTagPrefixes,
17345
+ scopedTagPrefixes,
17311
17346
  logger: context.logger
17312
17347
  }, { db: {
17313
17348
  fabfiles: context.db.fabfiles,
@@ -17381,7 +17416,7 @@ const knowledgeBaseSearchTool = {
17381
17416
  const semantic = await trySemanticKbSearch(context, query, tags, max_results);
17382
17417
  if (semantic) return semantic;
17383
17418
  try {
17384
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17419
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
17385
17420
  const searchResults = await context.db.fabfiles.search(context.userId, query, {
17386
17421
  tags: tags || [],
17387
17422
  type: file_type,
@@ -17398,6 +17433,7 @@ const knowledgeBaseSearchTool = {
17398
17433
  userGroups: context.user.groups || [],
17399
17434
  dataLakeTags,
17400
17435
  dataLakeTagPrefixes,
17436
+ scopedTagPrefixes,
17401
17437
  excludeContent: true
17402
17438
  });
17403
17439
  const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)));
@@ -17545,7 +17581,7 @@ const knowledgeBaseRetrieveTool = {
17545
17581
  if (files.length === 0) return `No document found with ID "${file_id}". The file may not exist or you may not have access to it. Try using search_knowledge_base to find the correct file ID.`;
17546
17582
  }
17547
17583
  if (files.length === 0 && (tags?.length || query)) {
17548
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17584
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
17549
17585
  files = (await context.db.fabfiles.search(context.userId, query || "", {
17550
17586
  tags: tags || [],
17551
17587
  shared: false
@@ -17561,6 +17597,7 @@ const knowledgeBaseRetrieveTool = {
17561
17597
  userGroups: context.user.groups || [],
17562
17598
  dataLakeTags,
17563
17599
  dataLakeTagPrefixes,
17600
+ scopedTagPrefixes,
17564
17601
  excludeContent: true
17565
17602
  })).data;
17566
17603
  if (files.length === 0) return `No documents found matching ${[query && `query "${query}"`, tags?.length && `tags [${tags.join(", ")}]`].filter(Boolean).join(" and ")}. Try broadening your search with search_knowledge_base.`;
@@ -17664,6 +17701,13 @@ function formatResult$1(result) {
17664
17701
  }
17665
17702
  return lines.join("\n");
17666
17703
  }
17704
+ /**
17705
+ * Returns a JSON `{ __uiSideEffect: true, ... }` envelope (see end of function), NOT
17706
+ * model-facing text. Production exec routes this through `wrapToolsForSentinels`
17707
+ * (sharedToolBuilder), which strips the envelope and surfaces `displayMessage` to the
17708
+ * model. Any future exec path that bypasses `wrapToolsForSentinels` would leak the raw
17709
+ * envelope into the model context — keep it behind that wrapper.
17710
+ */
17667
17711
  async function runQuantumSchedule(params) {
17668
17712
  const parseResult = SchedulingProblemSchema.safeParse(params.problem);
17669
17713
  if (!parseResult.success) return `Error: Invalid scheduling problem format.\n${parseResult.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nExpected format: { name, jobs: [{ id, name, operations: [{ jobId, machineId, duration }] }], machines: [{ id, name }] }`;
@@ -17709,7 +17753,12 @@ async function runQuantumSchedule(params) {
17709
17753
  lines.push("### Errors");
17710
17754
  errors.forEach((e) => lines.push(`- ${e}`));
17711
17755
  }
17712
- return lines.join("\n");
17756
+ return JSON.stringify({
17757
+ __uiSideEffect: true,
17758
+ type: "populateProblem",
17759
+ payload: problem,
17760
+ displayMessage: lines.join("\n")
17761
+ });
17713
17762
  }
17714
17763
  const quantumScheduleTool = {
17715
17764
  name: "quantum_schedule",
@@ -17723,7 +17772,9 @@ Available solvers: ${allSolvers.map((s) => `${s.id} (${s.name})`).join(", ")}.
17723
17772
 
17724
17773
  The "greedy" solver runs instantly. Metaheuristic solvers (simulated-annealing, tabu, genetic-algorithm, ant-colony) explore larger solution spaces but take longer. Use multiple solvers to race them against each other.
17725
17774
 
17726
- The problem must define jobs (each with ordered operations) and machines. Each operation runs on a specific machine for a specific duration. Operations within a job must run in order.`,
17775
+ The problem must define jobs (each with ordered operations) and machines. Each operation runs on a specific machine for a specific duration. Operations within a job must run in order.
17776
+
17777
+ Calling this loads the problem into the on-screen /opti Scheduling console (brief + Problem tab) so the user sees exactly what was solved and can tweak and re-race it — so the screen always matches the conversation.`,
17727
17778
  parameters: {
17728
17779
  type: "object",
17729
17780
  properties: {
@@ -17814,7 +17865,7 @@ The problem must define jobs (each with ordered operations) and machines. Each o
17814
17865
  }
17815
17866
  })
17816
17867
  };
17817
- const FAMILY_IDS = [
17868
+ const FAMILY_IDS$1 = [
17818
17869
  "routing",
17819
17870
  "packing",
17820
17871
  "assignment",
@@ -17825,7 +17876,7 @@ const FAMILY_IDS = [
17825
17876
  "continuous"
17826
17877
  ];
17827
17878
  /** Run the model and pull a JSON object out of the response (handles code fences). */
17828
- async function completeToJson(context, systemPrompt, userDescription) {
17879
+ async function completeToJson$1(context, systemPrompt, userDescription) {
17829
17880
  let rawContent = "";
17830
17881
  await context.llm.complete(context.model ?? "gpt-4", [{
17831
17882
  role: "system",
@@ -17860,6 +17911,7 @@ Rules:
17860
17911
  - "family" MUST be exactly "${familyId}".
17861
17912
  - Keep it small and browser-solvable, at a scale similar to the example.
17862
17913
  - ids are integers starting at 0; names are short human-readable strings.
17914
+ - "description" MUST be a single plain-English sentence summarizing the real-world scenario this models (what the user just described) — it is shown on the on-screen brief card so a human can recognize at a glance that it matches the conversation.
17863
17915
  - Output ONLY the JSON object — no prose, no markdown fences.`;
17864
17916
  }
17865
17917
  const quantumFormulateTool = {
@@ -17869,11 +17921,11 @@ const quantumFormulateTool = {
17869
17921
  const params = value;
17870
17922
  const userDescription = params.description?.trim();
17871
17923
  if (!userDescription) return "Error: Please provide a description of the optimization problem to formulate.";
17872
- if (params.familyId && !FAMILY_IDS.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS.join(", ")}. Omit familyId to formulate a job-shop scheduling problem.`;
17924
+ if (params.familyId && !FAMILY_IDS$1.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS$1.join(", ")}. Omit familyId to formulate a job-shop scheduling problem.`;
17873
17925
  const familyId = params.familyId || void 0;
17874
17926
  try {
17875
17927
  if (familyId) {
17876
- const result = await completeToJson(context, familyFormulationPrompt(familyId), userDescription);
17928
+ const result = await completeToJson$1(context, familyFormulationPrompt(familyId), userDescription);
17877
17929
  if ("error" in result) return `Error: ${result.error}\n\nPlease rephrase with more specifics for a ${familyId} problem.`;
17878
17930
  const validated = FAMILY_PROBLEM_SCHEMAS[familyId].safeParse(result.json);
17879
17931
  if (!validated.success) return `Error: the model produced an invalid ${familyId} problem.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease rephrase your description.`;
@@ -17882,9 +17934,9 @@ const quantumFormulateTool = {
17882
17934
  `## Formulated: "${problem.name}"`,
17883
17935
  problem.description ? `\n${problem.description}` : "",
17884
17936
  "",
17885
- `**${familyId} problem** — ready to load.`,
17937
+ `**${familyId} problem** — loaded into your active brief.`,
17886
17938
  "",
17887
- `Open the ${familyId} console's Problem tab and click "Load problem" on the pending banner to review, tweak, and race solvers.`
17939
+ `I've opened the ${familyId} console and set this as the active brief (use Undo on the banner to revert). Tweak it on the Problem tab or open Solvers to race it.`
17888
17940
  ].filter((l) => l !== void 0).join("\n");
17889
17941
  return JSON.stringify({
17890
17942
  __uiSideEffect: true,
@@ -17896,7 +17948,7 @@ const quantumFormulateTool = {
17896
17948
  displayMessage: summary
17897
17949
  });
17898
17950
  }
17899
- const result = await completeToJson(context, PROBLEM_FORMULATION_PROMPT, userDescription);
17951
+ const result = await completeToJson$1(context, PROBLEM_FORMULATION_PROMPT, userDescription);
17900
17952
  if ("error" in result) return `Error: ${result.error}\n\nPlease try rephrasing with specific jobs, machines, and processing times.`;
17901
17953
  const validated = SchedulingProblemSchema.safeParse(result.json);
17902
17954
  if (!validated.success) return `Error: LLM generated an invalid problem structure.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease try rephrasing your description.`;
@@ -17920,7 +17972,7 @@ const quantumFormulateTool = {
17920
17972
  "### Machines:",
17921
17973
  ...problem.machines.map((m) => `- ${m.name} (id: ${m.id})`),
17922
17974
  "",
17923
- "A problem is ready to load. Navigate to the Problem tab and click \"Load Problem\" to review, tweak, and run solvers."
17975
+ "I've loaded this as your active scheduling brief (use Undo on the banner to revert). Tweak it on the Problem tab or open Solvers to race it."
17924
17976
  ].filter((l) => l !== void 0).join("\n");
17925
17977
  return JSON.stringify({
17926
17978
  __uiSideEffect: true,
@@ -17952,7 +18004,7 @@ Describe the scenario in plain English — the entities, resources, costs/values
17952
18004
  },
17953
18005
  familyId: {
17954
18006
  type: "string",
17955
- enum: FAMILY_IDS,
18007
+ enum: FAMILY_IDS$1,
17956
18008
  description: "Optional optimization family. Omit for job-shop scheduling; otherwise one of the eight families."
17957
18009
  }
17958
18010
  },
@@ -17962,6 +18014,160 @@ Describe the scenario in plain English — the entities, resources, costs/values
17962
18014
  }
17963
18015
  })
17964
18016
  };
18017
+ const FAMILY_IDS = [
18018
+ "routing",
18019
+ "packing",
18020
+ "assignment",
18021
+ "network",
18022
+ "partitioning",
18023
+ "selection",
18024
+ "economic",
18025
+ "continuous"
18026
+ ];
18027
+ /** Run the model and pull a JSON object out of the response (handles code fences). */
18028
+ async function completeToJson(context, systemPrompt, userInstruction) {
18029
+ let rawContent = "";
18030
+ await context.llm.complete(context.model ?? "gpt-4", [{
18031
+ role: "system",
18032
+ content: systemPrompt
18033
+ }, {
18034
+ role: "user",
18035
+ content: userInstruction
18036
+ }], {
18037
+ temperature: .2,
18038
+ stream: false
18039
+ }, async (texts) => {
18040
+ rawContent = texts.filter((t) => t !== null && t !== void 0).join("");
18041
+ });
18042
+ let jsonStr = rawContent.trim();
18043
+ const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
18044
+ if (codeBlockMatch) jsonStr = codeBlockMatch[1].trim();
18045
+ try {
18046
+ return { json: JSON.parse(jsonStr) };
18047
+ } catch {
18048
+ return { error: `Could not parse the model response as JSON. Raw response:\n${rawContent}` };
18049
+ }
18050
+ }
18051
+ /**
18052
+ * Edit prompt. The current problem doubles as the shape template — the model must
18053
+ * return the SAME shape with only the requested change applied. The hard rule is
18054
+ * fidelity: anything the instruction doesn't touch comes back byte-for-byte, which
18055
+ * is the whole reason this tool exists instead of re-running `quantum_formulate`.
18056
+ */
18057
+ function editPrompt(currentProblem, familyId) {
18058
+ return `You are EDITING an existing optimization problem. Below is the CURRENT problem as JSON.
18059
+
18060
+ Apply ONLY the change the user describes. Preserve every other field EXACTLY — identical ids, names, and numeric values for anything the change does not explicitly touch. Do not renumber, rename, reorder, or "improve" untouched entities. Keep it small and browser-solvable.
18061
+ ${familyId ? `- "family" MUST stay exactly "${familyId}".` : "- Keep the job-shop scheduling shape (jobs with ordered operations referencing machine ids)."}
18062
+ - "description" stays a single plain-English sentence; update it only if the change makes it inaccurate.
18063
+
18064
+ CURRENT PROBLEM:
18065
+ ${JSON.stringify(currentProblem, null, 2)}
18066
+
18067
+ Output ONLY a JSON object of the form:
18068
+ { "problem": <the COMPLETE updated problem, same shape as above>, "changes": "<one short sentence describing exactly what you changed>" }
18069
+ No prose, no markdown fences.`;
18070
+ }
18071
+ /** Split the model output into the problem and an optional change summary. */
18072
+ function unwrap(json) {
18073
+ if (json && typeof json === "object" && "problem" in json) {
18074
+ const obj = json;
18075
+ return {
18076
+ problem: obj.problem,
18077
+ changes: typeof obj.changes === "string" ? obj.changes : void 0
18078
+ };
18079
+ }
18080
+ return { problem: json };
18081
+ }
18082
+ const quantumEditProblemTool = {
18083
+ name: "quantum_edit_problem",
18084
+ implementation: (context) => ({
18085
+ toolFn: async (value) => {
18086
+ const params = value;
18087
+ const instruction = params.instruction?.trim();
18088
+ if (!instruction) return "Error: Please provide an instruction describing the change to make.";
18089
+ if (params.currentProblem == null || typeof params.currentProblem !== "object") return "Error: Please pass the current problem (the active brief JSON) as `currentProblem` so it can be edited in place.";
18090
+ if (params.familyId && !FAMILY_IDS.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS.join(", ")}. Omit familyId to edit a job-shop scheduling problem.`;
18091
+ const familyId = params.familyId || void 0;
18092
+ try {
18093
+ const result = await completeToJson(context, editPrompt(params.currentProblem, familyId), instruction);
18094
+ if ("error" in result) return `Error: ${result.error}\n\nPlease restate the change more specifically.`;
18095
+ const { problem: rawProblem, changes } = unwrap(result.json);
18096
+ if (rawProblem == null || typeof rawProblem !== "object") return "Error: the edit did not produce a problem object. Please restate the change.";
18097
+ if (familyId) {
18098
+ const validated = FAMILY_PROBLEM_SCHEMAS[familyId].safeParse(rawProblem);
18099
+ if (!validated.success) return `Error: the edited ${familyId} problem is invalid.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease restate the change.`;
18100
+ const problem = validated.data;
18101
+ const summary = [
18102
+ `## Updated: "${problem.name}"`,
18103
+ changes ? `\n${changes}` : "",
18104
+ "",
18105
+ `**${familyId} problem** — applied to your active brief.`,
18106
+ "",
18107
+ `I've updated the ${familyId} brief in place (use Undo on the banner to revert). Open Solvers to race it.`
18108
+ ].filter((l) => l !== void 0).join("\n");
18109
+ return JSON.stringify({
18110
+ __uiSideEffect: true,
18111
+ type: "populateFamilyProblem",
18112
+ payload: {
18113
+ familyId,
18114
+ problem
18115
+ },
18116
+ displayMessage: summary
18117
+ });
18118
+ }
18119
+ const validated = SchedulingProblemSchema.safeParse(rawProblem);
18120
+ if (!validated.success) return `Error: the edited problem structure is invalid.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease restate the change.`;
18121
+ const problem = validated.data;
18122
+ const totalOps = problem.jobs.reduce((s, j) => s + j.operations.length, 0);
18123
+ const summary = [
18124
+ `## Updated: "${problem.name}"`,
18125
+ changes ? `\n${changes}` : "",
18126
+ "",
18127
+ `**${problem.jobs.length} jobs, ${problem.machines.length} machines, ${totalOps} operations**`,
18128
+ "",
18129
+ "I've updated your active scheduling brief in place (use Undo on the banner to revert). Open Solvers to race it."
18130
+ ].filter((l) => l !== void 0).join("\n");
18131
+ return JSON.stringify({
18132
+ __uiSideEffect: true,
18133
+ type: "populateProblem",
18134
+ payload: problem,
18135
+ displayMessage: summary
18136
+ });
18137
+ } catch (err) {
18138
+ return `Error editing problem: ${err instanceof Error ? err.message : String(err)}`;
18139
+ }
18140
+ },
18141
+ toolSchema: {
18142
+ name: "quantum_edit_problem",
18143
+ description: `Edit the optimization problem currently loaded in the /opti active brief. Use this — NOT quantum_formulate — whenever the user asks to change, refine, tweak, add to, or adjust the problem they're already looking at.
18144
+
18145
+ Pass the active brief as \`currentProblem\` (you have it in your context), describe the change in \`instruction\`, and pass \`familyId\` for one of the eight pattern families (omit for job-shop scheduling). The tool applies only the requested change, preserves everything else exactly, and the result is auto-applied to the active brief with one-click Undo. Use quantum_formulate instead only for a brand-new, unrelated problem.`,
18146
+ strict: false,
18147
+ parameters: {
18148
+ type: "object",
18149
+ properties: {
18150
+ instruction: {
18151
+ type: "string",
18152
+ description: "The change to apply to the current problem, in plain English (e.g. \"add two more jobs\")."
18153
+ },
18154
+ currentProblem: {
18155
+ type: "object",
18156
+ description: "The current problem JSON (the active brief) to edit in place.",
18157
+ additionalProperties: true
18158
+ },
18159
+ familyId: {
18160
+ type: "string",
18161
+ enum: FAMILY_IDS,
18162
+ description: "Optional optimization family. Omit for job-shop scheduling; otherwise one of the eight."
18163
+ }
18164
+ },
18165
+ required: ["instruction", "currentProblem"],
18166
+ additionalProperties: false
18167
+ }
18168
+ }
18169
+ })
18170
+ };
17965
18171
  const navigateViewTool = {
17966
18172
  name: "navigate_view",
17967
18173
  implementation: (context) => ({
@@ -18980,15 +19186,28 @@ DO NOT USE FOR:
18980
19186
  }
18981
19187
  })
18982
19188
  };
19189
+ /** Cap on how many invocable skills are listed back when resolution fails. */
19190
+ const INVOCABLE_HINT_LIMIT = 50;
18983
19191
  /**
18984
19192
  * LLM-invocable `skill` tool — the autonomous counterpart to user `/skill-name`
18985
19193
  * invocations handled by SkillsFeature.
18986
19194
  *
18987
- * The LLM passes `{ name, args }`. The tool resolves the skill in the caller's
18988
- * scope (`findByNameForUser`), expands `$ARGUMENTS` / `$1` / `$2`, and returns
18989
- * the expanded body as the observation for the next turn. Skills with
19195
+ * The LLM passes `{ name, args }`. The tool resolves the skill across every
19196
+ * scope the caller can access (`findAccessibleByNameForUser` owned + shared +
19197
+ * global-read), expands `$ARGUMENTS` / `$1` / `$2`, and returns the expanded
19198
+ * body as the observation for the next turn. Skills with
18990
19199
  * `disableModelInvocation: true` are not callable here.
18991
19200
  *
19201
+ * Security: a skill the caller does NOT own (shared / org / global) is treated
19202
+ * as untrusted content — its body is wrapped with explicit delimiters and a
19203
+ * "do not follow conflicting instructions" framing so a malicious author can't
19204
+ * hijack the turn via prompt injection. The skill's `allowedTools` whitelist is
19205
+ * surfaced to the model as a *requested* tool scope (intersected with the
19206
+ * invoker's allow-list via `intersectAllowedTools`). NOTE: today this is prompt
19207
+ * guidance only — there is no `ToolContext` invoker allow-list to enforce it
19208
+ * against, so the wording stays advisory. When that wiring lands, the same
19209
+ * `permittedTools` set can narrow the completion's actual tool list.
19210
+ *
18992
19211
  * Catalog discovery: the available skill names + descriptions are listed in
18993
19212
  * the system prompt by SkillsFeature, so the LLM knows what's invocable. If
18994
19213
  * the LLM passes a name that isn't accessible, the tool returns an error
@@ -19004,14 +19223,18 @@ const skillTool = {
19004
19223
  const name = rawName.replace(/^\//, "");
19005
19224
  const skillsRepo = context.db.skills;
19006
19225
  if (!skillsRepo) return "Error: skills are not configured in this environment.";
19007
- const skill = await skillsRepo.findByNameForUser(context.userId, name);
19226
+ const skill = await skillsRepo.findAccessibleByNameForUser(context.userId, name);
19008
19227
  if (!skill || skill.disableModelInvocation) {
19009
- const invocable = (await skillsRepo.listForUser(context.userId)).filter((s) => !s.disableModelInvocation).map((s) => s.name);
19228
+ const invocable = (await skillsRepo.listAccessibleInvocableForUser(context.userId, INVOCABLE_HINT_LIMIT)).map((s) => s.name);
19010
19229
  return invocable.length === 0 ? `Error: skill "${name}" is not available, and you have no LLM-invocable skills defined.` : `Error: skill "${name}" is not available. Invocable skills: ${invocable.join(", ")}`;
19011
19230
  }
19012
19231
  const argArray = typeof params.args === "string" ? parseSkillArguments(params.args) : [];
19013
19232
  const expandedBody = substituteArguments$1(skill.body, argArray);
19014
- return `## Skill: /${skill.name}\n\n${expandedBody}\n\n---\nFollow the instructions above for the rest of this turn.`;
19233
+ if (skill.userId === context.userId) return `## Skill: /${skill.name}\n\n${expandedBody}\n\n---\nFollow the instructions above for the rest of this turn.`;
19234
+ const ownerLabel = skill.organizationId ? "your organization" : skill.isSystem ? "the system" : "another user";
19235
+ const wrapped = wrapUntrustedSkillBody(skill.name, expandedBody, ownerLabel);
19236
+ const permittedTools = intersectAllowedTools(skill.allowedTools, void 0);
19237
+ return `${wrapped}${permittedTools && permittedTools.length > 0 ? `\n\nThis skill is intended to use only these tools: ${permittedTools.join(", ")}. Prefer to stay within that set.` : ""}\n\n---\nFollow the intent above for the rest of this turn, subject to the constraints stated.`;
19015
19238
  },
19016
19239
  toolSchema: {
19017
19240
  name: "skill",
@@ -19875,6 +20098,7 @@ const b4mTools = {
19875
20098
  retrieve_knowledge_content: knowledgeBaseRetrieveTool,
19876
20099
  quantum_schedule: quantumScheduleTool,
19877
20100
  quantum_formulate: quantumFormulateTool,
20101
+ quantum_edit_problem: quantumEditProblemTool,
19878
20102
  navigate_view: navigateViewTool,
19879
20103
  generate_jupyter_notebook: jupyterNotebookTool,
19880
20104
  excel_generation: excelGenerationTool,
@@ -22358,6 +22582,159 @@ parentPort.on('message', async (msg) => {
22358
22582
  }
22359
22583
  });
22360
22584
  `;
22585
+ String.raw`
22586
+ const __cap = _captureLine;
22587
+ const __callTool = _callTool;
22588
+ delete globalThis._captureLine;
22589
+ delete globalThis._callTool;
22590
+
22591
+ const HARD_PER_LINE_BYTES = ${5e4};
22592
+
22593
+ function __jsonReplacer(_k, v) {
22594
+ if (v instanceof Error) return { name: v.name, message: v.message };
22595
+ if (typeof v === 'bigint') return v.toString() + 'n';
22596
+ return v;
22597
+ }
22598
+ function __formatLine(args) {
22599
+ const line = args.map(a => {
22600
+ if (typeof a === 'string') return a;
22601
+ if (a === undefined) return 'undefined';
22602
+ if (a === null) return 'null';
22603
+ try { return JSON.stringify(a, __jsonReplacer, 2); } catch { return String(a); }
22604
+ }).join(' ');
22605
+ return line.length > HARD_PER_LINE_BYTES
22606
+ ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
22607
+ : line;
22608
+ }
22609
+ globalThis.console = {
22610
+ log: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22611
+ warn: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22612
+ error: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22613
+ info: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22614
+ };
22615
+
22616
+ // A bare isolate has no structuredClone (it's a host/web API, not a V8
22617
+ // intrinsic). The in-process + worker backends expose the *host's* real
22618
+ // structuredClone — which we can't reach across the isolate boundary — so
22619
+ // polyfill the structured-clone algorithm in-isolate for the common cloneable
22620
+ // types (Date, RegExp, Map, Set, ArrayBuffer / typed arrays / DataView,
22621
+ // arrays, plain objects) WITH circular-reference support. This is real
22622
+ // parity for those types; a JSON round-trip would silently drop
22623
+ // Date -> string, Map/Set -> {}, RegExp -> {}, TypedArray -> index map.
22624
+ // Genuinely uncloneable inputs (functions, etc.) throw, as the real API does.
22625
+ if (typeof globalThis.structuredClone !== 'function') {
22626
+ globalThis.structuredClone = function structuredClone(input) {
22627
+ const seen = new WeakMap();
22628
+ function clone(v) {
22629
+ if (v === null || typeof v !== 'object') {
22630
+ if (typeof v === 'function') throw new Error('structuredClone: a function could not be cloned');
22631
+ return v;
22632
+ }
22633
+ if (seen.has(v)) return seen.get(v);
22634
+ // Every cloneable type records its clone in the seen-map BEFORE returning,
22635
+ // so the same object appearing at multiple paths yields one shared clone
22636
+ // (reference identity), matching the real structured-clone algorithm —
22637
+ // not just the recursive containers.
22638
+ if (v instanceof Date) { const out = new Date(v.getTime()); seen.set(v, out); return out; }
22639
+ if (v instanceof RegExp) { const out = new RegExp(v.source, v.flags); seen.set(v, out); return out; }
22640
+ if (v instanceof ArrayBuffer) { const out = v.slice(0); seen.set(v, out); return out; }
22641
+ if (typeof DataView !== 'undefined' && v instanceof DataView) {
22642
+ const out = new DataView(clone(v.buffer), v.byteOffset, v.byteLength); seen.set(v, out); return out;
22643
+ }
22644
+ if (ArrayBuffer.isView(v)) { const out = new v.constructor(v); seen.set(v, out); return out; } // typed array — fresh buffer
22645
+ if (v instanceof Map) {
22646
+ const out = new Map(); seen.set(v, out);
22647
+ for (const [k, val] of v) out.set(clone(k), clone(val));
22648
+ return out;
22649
+ }
22650
+ if (v instanceof Set) {
22651
+ const out = new Set(); seen.set(v, out);
22652
+ for (const val of v) out.add(clone(val));
22653
+ return out;
22654
+ }
22655
+ if (Array.isArray(v)) {
22656
+ const out = []; seen.set(v, out);
22657
+ for (let i = 0; i < v.length; i++) out[i] = clone(v[i]);
22658
+ return out;
22659
+ }
22660
+ const out = {}; seen.set(v, out);
22661
+ for (const k of Object.keys(v)) out[k] = clone(v[k]);
22662
+ return out;
22663
+ }
22664
+ return clone(input);
22665
+ };
22666
+ }
22667
+
22668
+ // Neuter string-based code generation. Same posture as the worker /
22669
+ // in-process backends' codeGeneration:{strings:false} (Ken's P2 #2): the
22670
+ // LLM has no legitimate reason to generate second-order code that's
22671
+ // invisible in the logged 'code' parameter. isolated-vm exposes no V8-level
22672
+ // codegen toggle, so we block every reachable path to the intrinsic
22673
+ // Function constructors — not just the global bindings. The well-known
22674
+ // vm-escape vector \`(function(){}).constructor("…")()\` reaches the
22675
+ // intrinsic via the prototype chain, around \`globalThis.Function\`, so we
22676
+ // capture each function-type intrinsic BEFORE overriding the globals and
22677
+ // replace its \`prototype.constructor\` too. (Generated code would stay
22678
+ // trapped in the isolate regardless — this is auditability + worker parity,
22679
+ // not the core isolation guarantee, which the isolate itself provides.)
22680
+ const __RealFunction = (function () {}).constructor;
22681
+ const __AsyncFunction = (async function () {}).constructor;
22682
+ const __GeneratorFunction = (function* () {}).constructor;
22683
+ const __AsyncGeneratorFunction = (async function* () {}).constructor;
22684
+ const __blockCodegen = function () {
22685
+ throw new Error('code generation from strings (eval / Function) is disabled in the REPL sandbox');
22686
+ };
22687
+ for (const __Ctor of [__RealFunction, __AsyncFunction, __GeneratorFunction, __AsyncGeneratorFunction]) {
22688
+ try {
22689
+ Object.defineProperty(__Ctor.prototype, 'constructor', {
22690
+ value: __blockCodegen,
22691
+ writable: true,
22692
+ configurable: true,
22693
+ enumerable: false,
22694
+ });
22695
+ } catch (_e) {
22696
+ // best-effort; the global overrides below still cover the common paths
22697
+ }
22698
+ }
22699
+ globalThis.eval = __blockCodegen;
22700
+ globalThis.Function = __blockCodegen;
22701
+
22702
+ // Tool-stub registry. Each registered tool becomes a top-level async
22703
+ // function that round-trips through the host dispatcher and re-throws on
22704
+ // the { ok:false } envelope.
22705
+ globalThis.__registerTools = function (names) {
22706
+ for (const name of names) {
22707
+ globalThis[name] = async (...args) => {
22708
+ const envJson = await __callTool.apply(
22709
+ undefined,
22710
+ [name, JSON.stringify(args)],
22711
+ { arguments: { copy: true }, result: { promise: true, copy: true } }
22712
+ );
22713
+ const env = JSON.parse(envJson);
22714
+ if (!env.ok) throw new Error(env.error);
22715
+ return env.value;
22716
+ };
22717
+ }
22718
+ };
22719
+ `;
22720
+ z.object({
22721
+ reflection: z.string().min(1),
22722
+ summary: z.string().min(1),
22723
+ nextIntendedAction: z.string().min(1),
22724
+ scopeLocks: z.array(z.string()).default([]),
22725
+ drivesAfter: DriveVectorSchema
22726
+ });
22727
+ z.object({
22728
+ semanticMemory: z.array(z.object({
22729
+ id: z.string().min(1),
22730
+ fact: z.string().min(1),
22731
+ evidenceTier: EvidenceTierSchema,
22732
+ confidence: z.number().min(0).max(1).default(.5),
22733
+ sourceEpisodeIds: z.array(z.string()).default([]),
22734
+ lastAffirmedAt: z.string()
22735
+ })),
22736
+ openQuestions: z.array(z.string()).default([])
22737
+ });
22361
22738
  z.object({
22362
22739
  count: z.number().optional(),
22363
22740
  unlimitedUse: z.boolean().optional(),
@@ -22873,6 +23250,18 @@ z.object({
22873
23250
  id: z.string().min(1) });
22874
23251
  z.object({ organizationId: z.string() });
22875
23252
  z.object({ id: z.string() });
23253
+ const createPromptSchema = z.object({
23254
+ type: z.string(),
23255
+ name: z.string(),
23256
+ promptText: z.string(),
23257
+ tags: z.array(z.string())
23258
+ });
23259
+ z.object({ id: z.string() });
23260
+ z.object({ id: z.string() });
23261
+ z.object({ name: z.string() });
23262
+ z.object({ tags: z.array(z.string()) });
23263
+ z.object({ type: z.string() });
23264
+ createPromptSchema.partial().extend({ id: z.string() });
22876
23265
  z.object({ id: z.string() });
22877
23266
  z.object({ ids: z.array(z.string()).optional() });
22878
23267
  z.object({
@@ -22885,7 +23274,7 @@ z.object({
22885
23274
  systemPriority: z.number().min(0).max(999).optional(),
22886
23275
  sessionId: z.string().optional(),
22887
23276
  notes: z.string().optional(),
22888
- primaryTag: z.string().optional(),
23277
+ primaryTag: z.string().nullable().optional(),
22889
23278
  tags: z.array(z.object({
22890
23279
  name: z.string(),
22891
23280
  strength: z.number()
@@ -22943,6 +23332,7 @@ z.object({
22943
23332
  userGroups: z.array(z.string()).optional(),
22944
23333
  dataLakeTags: z.array(z.string()).optional(),
22945
23334
  dataLakeTagPrefixes: z.array(z.string()).optional(),
23335
+ scopedTagPrefixes: z.array(z.string()).optional(),
22946
23336
  excludeContent: z.coerce.boolean().optional()
22947
23337
  }).optional()
22948
23338
  });
@@ -24294,6 +24684,15 @@ z.object({
24294
24684
  isPersonal: z.boolean(),
24295
24685
  mementos: z.array(SingleMementoEvalSchema).optional()
24296
24686
  });
24687
+ new Set([{
24688
+ key: "internal",
24689
+ audienceType: "internal",
24690
+ label: "Internal"
24691
+ }, {
24692
+ key: "customer",
24693
+ audienceType: "customer",
24694
+ label: "Customer"
24695
+ }].map((v) => v.key));
24297
24696
  crypto_exports.safeCompareTokens;
24298
24697
  //#endregion
24299
24698
  //#region ../../b4m-core/services/dist/llm/tools/cliTools.mjs
@@ -27392,16 +27791,21 @@ function extractUsageInfo(parsed) {
27392
27791
  return {
27393
27792
  inputTokens: parsed.usage?.inputTokens,
27394
27793
  outputTokens: parsed.usage?.outputTokens,
27794
+ cacheReadInputTokens: parsed.usage?.cacheReadInputTokens,
27795
+ cacheCreationInputTokens: parsed.usage?.cacheCreationInputTokens,
27395
27796
  creditsUsed: parsed.credits?.used,
27396
27797
  usdCost: parsed.credits?.usdCost
27397
27798
  };
27398
27799
  }
27399
27800
  /**
27400
- * Accumulates streaming LLM response chunks (text, tool calls, thinking blocks, usage)
27401
- * and fires the completion callback once at the end.
27801
+ * Reduces a stream of {@link StreamEvent}s (text, tool calls, thinking blocks,
27802
+ * usage) into a single accumulated assistant turn, then fires the completion
27803
+ * callback once at the end.
27402
27804
  *
27403
- * Shared between ServerLlmBackend (SSE) and WebSocketLlmBackend (WebSocket frames)
27404
- * so accumulation logic lives in exactly one place.
27805
+ * Both backends — `ServerLlmBackend` (SSE) and `WebSocketLlmBackend` (WebSocket
27806
+ * frames) decode their wire payloads into the shared event union and feed
27807
+ * each event to {@link apply}, so accumulation logic lives in exactly one
27808
+ * place and is transport-agnostic.
27405
27809
  */
27406
27810
  var StreamAccumulator = class {
27407
27811
  constructor() {
@@ -27410,21 +27814,25 @@ var StreamAccumulator = class {
27410
27814
  this.thinkingBlocks = [];
27411
27815
  this.lastUsageInfo = {};
27412
27816
  }
27413
- onContent(text, usage, credits) {
27414
- this.accumulatedText += text;
27415
- if (usage || credits) this.lastUsageInfo = extractUsageInfo({
27416
- usage,
27417
- credits
27418
- });
27419
- }
27420
- onToolUse(text, tools, thinking, usage, credits) {
27421
- if (text) this.accumulatedText += text;
27422
- if (tools && tools.length > 0) this.toolsUsed = tools;
27423
- if (thinking && thinking.length > 0) this.thinkingBlocks = thinking;
27424
- if (usage || credits) this.lastUsageInfo = extractUsageInfo({
27425
- usage,
27426
- credits
27427
- });
27817
+ /**
27818
+ * Fold one streaming event into the accumulated turn. `error` events carry no
27819
+ * accumulable content backends surface them by rejecting — so they are a
27820
+ * no-op here.
27821
+ */
27822
+ apply(event) {
27823
+ switch (event.type) {
27824
+ case "content":
27825
+ this.accumulatedText += event.text ?? "";
27826
+ if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27827
+ break;
27828
+ case "tool_use":
27829
+ if (event.text) this.accumulatedText += event.text;
27830
+ if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
27831
+ if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
27832
+ if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27833
+ break;
27834
+ case "error": break;
27835
+ }
27428
27836
  }
27429
27837
  /** True when neither text nor tools have been accumulated (stream produced nothing useful). */
27430
27838
  isEmpty() {
@@ -27457,6 +27865,93 @@ var StreamAccumulator = class {
27457
27865
  }
27458
27866
  };
27459
27867
  //#endregion
27868
+ //#region src/llm/streamEvents.ts
27869
+ /**
27870
+ * Unified streaming event protocol for CLI LLM backends.
27871
+ *
27872
+ * Both transports — `ServerLlmBackend` (SSE) and `WebSocketLlmBackend`
27873
+ * (HTTP POST + WebSocket frames) — decode their wire payloads into this single
27874
+ * typed union, then feed it to `StreamAccumulator`. This collapses the two
27875
+ * previously-duplicated, loosely-typed chunk shapes (`SSEEvent` in
27876
+ * ServerLlmBackend, the inline `chunk` type in WebSocketLlmBackend) into one
27877
+ * source of truth, and replaces the unchecked `as SSEEvent` cast on
27878
+ * server-provided data with a runtime-validated parse at the boundary.
27879
+ *
27880
+ * The vocabulary mirrors what the backends emit today (`content` / `tool_use` /
27881
+ * `error`) so the refactor changes no observable behavior. Finer-grained delta
27882
+ * events (text_delta / toolcall_delta / …) are intentionally out of scope — the
27883
+ * CLI accumulates a full turn before firing its callback, so per-delta events
27884
+ * would be a behavior change, not a refactor.
27885
+ */
27886
+ /**
27887
+ * Token usage counts carried alongside content/tool_use events. Shape mirrors
27888
+ * `SSEContentEvent.usage` in `@bike4mind/common` (`sseEvents.ts`) — the
27889
+ * authoritative wire contract — including the Anthropic cache-token deltas
27890
+ * (#8155) so they survive the boundary parse instead of being stripped.
27891
+ */
27892
+ const usageSchema = z.object({
27893
+ inputTokens: z.number().optional(),
27894
+ outputTokens: z.number().optional(),
27895
+ cacheReadInputTokens: z.number().optional(),
27896
+ cacheCreationInputTokens: z.number().optional()
27897
+ }).partial();
27898
+ /** Credit/cost accounting carried alongside content/tool_use events (SSE only). */
27899
+ const creditsSchema = z.object({
27900
+ used: z.number().optional(),
27901
+ usdCost: z.number().optional()
27902
+ }).partial();
27903
+ /**
27904
+ * A single tool call requested by the model. Shape mirrors the wire contract in
27905
+ * `@bike4mind/common` (`SSEContentEvent.tools` / `CompletionInfo.toolsUsed`):
27906
+ * the server sends `{ name, arguments?: string, id? }` — `arguments` is a raw
27907
+ * JSON string, NOT a parsed object. (The previous `input: Record<…>` shape was
27908
+ * a typed lie the old unchecked `as SSEEvent` cast hid; downstream consumers
27909
+ * read `tool.arguments`, e.g. `toolParallelizer.ts`.)
27910
+ */
27911
+ const toolUseSchema = z.object({
27912
+ name: z.string(),
27913
+ arguments: z.string().optional(),
27914
+ id: z.string().optional()
27915
+ });
27916
+ /**
27917
+ * Discriminated union of streaming events. `thinking` blocks are opaque
27918
+ * provider-shaped objects (Anthropic extended thinking) replayed verbatim into
27919
+ * the next request, so they stay `unknown[]` rather than being modeled.
27920
+ */
27921
+ const streamEventSchema = z.discriminatedUnion("type", [
27922
+ z.object({
27923
+ type: z.literal("content"),
27924
+ text: z.string().optional(),
27925
+ usage: usageSchema.optional(),
27926
+ credits: creditsSchema.optional()
27927
+ }),
27928
+ z.object({
27929
+ type: z.literal("tool_use"),
27930
+ text: z.string().optional(),
27931
+ tools: z.array(toolUseSchema).optional(),
27932
+ thinking: z.array(z.unknown()).optional(),
27933
+ usage: usageSchema.optional(),
27934
+ credits: creditsSchema.optional()
27935
+ }),
27936
+ z.object({
27937
+ type: z.literal("error"),
27938
+ message: z.string().optional()
27939
+ })
27940
+ ]);
27941
+ /**
27942
+ * Validate an already-JSON-decoded wire payload against the event union.
27943
+ *
27944
+ * Returns the typed event on success, or `null` when the payload does not match
27945
+ * a known event shape. A `null` result is treated as "skip this event" by the
27946
+ * backends — preserving the prior behavior where an unrecognized `type` simply
27947
+ * fell through unhandled. (Malformed JSON is still caught upstream at the
27948
+ * `JSON.parse` boundary.)
27949
+ */
27950
+ function parseStreamEvent(data) {
27951
+ const result = streamEventSchema.safeParse(data);
27952
+ return result.success ? result.data : null;
27953
+ }
27954
+ //#endregion
27460
27955
  //#region src/llm/ServerLlmBackend.ts
27461
27956
  /**
27462
27957
  * Connection-level failures that should be retried rather than surfaced to the
@@ -27631,30 +28126,29 @@ var ServerLlmBackend = class ServerLlmBackend {
27631
28126
  return;
27632
28127
  }
27633
28128
  try {
27634
- const parsed = JSON.parse(data);
27635
- if (parsed.type === "error") {
28129
+ const event = parseStreamEvent(JSON.parse(data));
28130
+ if (!event) return;
28131
+ if (event.type === "error") {
27636
28132
  receivedError = true;
27637
- streamLogger.onCriticalEvent(eventCount, "ERROR", parsed.message || "Server error");
27638
- reject(new Error(parsed.message || "Server error"));
28133
+ streamLogger.onCriticalEvent(eventCount, "ERROR", event.message || "Server error");
28134
+ reject(new Error(event.message || "Server error"));
27639
28135
  return;
27640
28136
  }
27641
- if (parsed.type === "content") {
27642
- const textChunk = parsed.text || "";
27643
- accumulator.onContent(textChunk, parsed.usage, parsed.credits);
27644
- streamLogger.onContent(eventCount, textChunk, accumulator.rawText);
27645
- } else if (parsed.type === "tool_use") {
27646
- streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${parsed.tools?.length}`);
27647
- if (parsed.tools && parsed.tools.length > 0) for (const tool of parsed.tools) {
28137
+ if (event.type === "content") {
28138
+ accumulator.apply(event);
28139
+ streamLogger.onContent(eventCount, event.text || "", accumulator.rawText);
28140
+ } else if (event.type === "tool_use") {
28141
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${event.tools?.length}`);
28142
+ if (event.tools && event.tools.length > 0) for (const tool of event.tools) {
27648
28143
  logger.debug(`TOOL REQUEST: ${tool.name}`);
27649
28144
  try {
27650
- const paramsStr = JSON.stringify(tool.input);
27651
- logger.debug(` Params: ${paramsStr}`);
28145
+ logger.debug(` Params: ${tool.arguments ?? "{}"}`);
27652
28146
  } catch {
27653
28147
  logger.debug(` Params: [Unable to stringify]`);
27654
28148
  }
27655
28149
  }
27656
- accumulator.onToolUse(parsed.text || "", parsed.tools, parsed.thinking, parsed.usage, parsed.credits);
27657
- if (parsed.thinking && parsed.thinking.length > 0) streamLogger.onCriticalEvent(eventCount, "THINKING", `${parsed.thinking.length} thinking blocks`);
28150
+ accumulator.apply(event);
28151
+ if (event.thinking && event.thinking.length > 0) streamLogger.onCriticalEvent(eventCount, "THINKING", `${event.thinking.length} thinking blocks`);
27658
28152
  }
27659
28153
  } catch (parseError) {
27660
28154
  streamLogger.streamError(parseError);
@@ -27835,20 +28329,22 @@ var FallbackLlmBackend = class {
27835
28329
  async complete(model, messages, options, callback) {
27836
28330
  if (options.abortSignal?.aborted) return this.inner.complete(model, messages, options, callback);
27837
28331
  const modelsToTry = [model, ...this.fallbackModels.filter((m) => m !== model)];
27838
- let callbackInvoked = false;
27839
- const guardedCallback = async (text, completionInfo) => {
27840
- callbackInvoked = true;
27841
- return callback(text, completionInfo);
27842
- };
27843
28332
  let lastError;
27844
28333
  for (let i = 0; i < modelsToTry.length; i++) {
27845
28334
  const modelToTry = modelsToTry[i];
28335
+ const buffered = [];
28336
+ const bufferingCallback = async (text, completionInfo) => {
28337
+ buffered.push({
28338
+ text,
28339
+ completionInfo
28340
+ });
28341
+ };
27846
28342
  try {
27847
- await this.inner.complete(modelToTry, messages, options, guardedCallback);
28343
+ await this.inner.complete(modelToTry, messages, options, bufferingCallback);
28344
+ for (const delivery of buffered) await callback(delivery.text, delivery.completionInfo);
27848
28345
  return;
27849
28346
  } catch (error) {
27850
28347
  if (options.abortSignal?.aborted) throw error;
27851
- if (callbackInvoked) throw error;
27852
28348
  lastError = error instanceof Error ? error : new Error(String(error));
27853
28349
  const nextModel = modelsToTry[i + 1];
27854
28350
  if (nextModel) {
@@ -29755,12 +30251,14 @@ var WebSocketLlmBackend = class {
29755
30251
  eventCount++;
29756
30252
  const chunk = message.chunk;
29757
30253
  streamLogger.onEvent(eventCount, JSON.stringify(chunk));
29758
- if (chunk.type === "content") {
29759
- accumulator.onContent(chunk.text || "", chunk.usage);
29760
- streamLogger.onContent(eventCount, chunk.text || "", accumulator.rawText);
29761
- } else if (chunk.type === "tool_use") {
29762
- streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${chunk.tools?.length}`);
29763
- accumulator.onToolUse(chunk.text || "", chunk.tools, chunk.thinking, chunk.usage);
30254
+ const event = parseStreamEvent(chunk);
30255
+ if (!event) return;
30256
+ if (event.type === "content") {
30257
+ accumulator.apply(event);
30258
+ streamLogger.onContent(eventCount, event.text || "", accumulator.rawText);
30259
+ } else if (event.type === "tool_use") {
30260
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${event.tools?.length}`);
30261
+ accumulator.apply(event);
29764
30262
  }
29765
30263
  } else if (action === "cli_completion_done") {
29766
30264
  streamLogger.streamComplete(accumulator.rawText);