@sentry/junior 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -5,9 +5,8 @@ import {
5
5
  listCapabilityProviders,
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
- parseSkillInvocation,
9
- stripFrontmatter
10
- } from "./chunk-4XWTSMRF.js";
8
+ parseSkillInvocation
9
+ } from "./chunk-VJLT6LLV.js";
11
10
  import {
12
11
  SANDBOX_SKILLS_ROOT,
13
12
  SANDBOX_WORKSPACE_ROOT,
@@ -25,8 +24,9 @@ import {
25
24
  resolveRuntimeDependencySnapshot,
26
25
  runNonInteractiveCommand,
27
26
  sandboxSkillDir,
27
+ sandboxSkillFile,
28
28
  toOptionalTrimmed
29
- } from "./chunk-XYOKYK6U.js";
29
+ } from "./chunk-LOTYK7IE.js";
30
30
  import {
31
31
  CredentialUnavailableError,
32
32
  buildOAuthTokenRequest,
@@ -5021,7 +5021,7 @@ function toLoadedSkill(result, availableSkills) {
5021
5021
  return {
5022
5022
  name: result.skill_name,
5023
5023
  description: result.description,
5024
- skillPath: result.skill_dir,
5024
+ skillPath: metadata?.skillPath ?? result.skill_dir,
5025
5025
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
5026
5026
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
5027
5027
  ...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
@@ -5029,7 +5029,7 @@ function toLoadedSkill(result, availableSkills) {
5029
5029
  body: result.instructions
5030
5030
  };
5031
5031
  }
5032
- async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5032
+ async function loadSkillFromHost(availableSkills, skillName) {
5033
5033
  const requested = skillName.trim().toLowerCase();
5034
5034
  const skill = availableSkills.find(
5035
5035
  (entry) => entry.name.toLowerCase() === requested
@@ -5042,10 +5042,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5042
5042
  };
5043
5043
  }
5044
5044
  const skillDir = sandboxSkillDir(skill.name);
5045
- const skillFilePath = `${skillDir}/SKILL.md`;
5046
- const file = await sandbox.readFileToBuffer({ path: skillFilePath });
5047
- if (!file) {
5048
- throw new Error(`failed to read ${skillFilePath}`);
5045
+ const skillFilePath = sandboxSkillFile(skill.name);
5046
+ const [loaded] = await loadSkillsByName([skill.name], availableSkills);
5047
+ if (!loaded) {
5048
+ throw new Error(`failed to load ${skill.name}`);
5049
5049
  }
5050
5050
  return {
5051
5051
  ok: true,
@@ -5054,10 +5054,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5054
5054
  ...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
5055
5055
  skill_dir: skillDir,
5056
5056
  location: skillFilePath,
5057
- instructions: stripFrontmatter(file.toString("utf8"))
5057
+ instructions: loaded.body
5058
5058
  };
5059
5059
  }
5060
- function createLoadSkillTool(sandbox, availableSkills, options) {
5060
+ function createLoadSkillTool(availableSkills, options) {
5061
5061
  return tool({
5062
5062
  description: "Load a skill by name so its instructions are available for this turn. The result includes `requires_capabilities` when the skill declares authenticated provider access, and `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
5063
5063
  inputSchema: Type4.Object({
@@ -5067,11 +5067,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
5067
5067
  })
5068
5068
  }),
5069
5069
  execute: async ({ skill_name }) => {
5070
- const result = await loadSkillFromSandbox(
5071
- sandbox,
5072
- availableSkills,
5073
- skill_name
5074
- );
5070
+ const result = await loadSkillFromHost(availableSkills, skill_name);
5075
5071
  const loadedSkill = toLoadedSkill(result, availableSkills);
5076
5072
  if (loadedSkill) {
5077
5073
  const metadata = await options?.onSkillLoaded?.(loadedSkill);
@@ -6810,7 +6806,7 @@ function createToolState(hooks, context) {
6810
6806
  function createTools(availableSkills, hooks = {}, context) {
6811
6807
  const state = createToolState(hooks, context);
6812
6808
  const tools = {
6813
- loadSkill: createLoadSkillTool(context.sandbox, availableSkills, {
6809
+ loadSkill: createLoadSkillTool(availableSkills, {
6814
6810
  onSkillLoaded: hooks.onSkillLoaded
6815
6811
  }),
6816
6812
  systemTime: createSystemTimeTool(),
@@ -6866,10 +6862,7 @@ function resolveChannelCapabilities(channelId) {
6866
6862
  }
6867
6863
 
6868
6864
  // src/chat/sandbox/sandbox.ts
6869
- import fs3 from "fs/promises";
6870
- import path4 from "path";
6871
- import { Sandbox } from "@vercel/sandbox";
6872
- import { createBashTool as createBashTool2 } from "bash-tool";
6865
+ import fs4 from "fs/promises";
6873
6866
 
6874
6867
  // src/chat/sandbox/http-error-details.ts
6875
6868
  var DEFAULT_PREVIEW_LIMIT = 512;
@@ -6973,6 +6966,109 @@ function extractHttpErrorDetails(error, options = {}) {
6973
6966
  };
6974
6967
  }
6975
6968
 
6969
+ // src/chat/sandbox/errors.ts
6970
+ var SANDBOX_ERROR_FIELDS = [
6971
+ {
6972
+ sourceKey: "sandboxId",
6973
+ attributeKey: "sandbox_id",
6974
+ summaryKey: "sandboxId"
6975
+ }
6976
+ ];
6977
+ function getSandboxErrorDetails(error) {
6978
+ return extractHttpErrorDetails(error, {
6979
+ attributePrefix: "app.sandbox.api_error",
6980
+ extraFields: [...SANDBOX_ERROR_FIELDS]
6981
+ });
6982
+ }
6983
+ function findInErrorChain(error, predicate) {
6984
+ const seen = /* @__PURE__ */ new Set();
6985
+ let current = error;
6986
+ while (current && !seen.has(current)) {
6987
+ if (predicate(current)) {
6988
+ return true;
6989
+ }
6990
+ seen.add(current);
6991
+ current = typeof current === "object" ? current.cause : void 0;
6992
+ }
6993
+ return false;
6994
+ }
6995
+ function getFirstErrorMessage(error) {
6996
+ const seen = /* @__PURE__ */ new Set();
6997
+ let current = error;
6998
+ while (current && !seen.has(current)) {
6999
+ if (current instanceof Error) {
7000
+ const message = current.message.trim();
7001
+ if (message) {
7002
+ return message;
7003
+ }
7004
+ }
7005
+ seen.add(current);
7006
+ current = typeof current === "object" ? current.cause : void 0;
7007
+ }
7008
+ return void 0;
7009
+ }
7010
+ function isAlreadyExistsError(error) {
7011
+ const details = getSandboxErrorDetails(error);
7012
+ return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7013
+ }
7014
+ function isSandboxUnavailableError(error) {
7015
+ return findInErrorChain(error, (candidate) => {
7016
+ const details = getSandboxErrorDetails(candidate);
7017
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7018
+ return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7019
+ });
7020
+ }
7021
+ function isSnapshottingError(error) {
7022
+ return findInErrorChain(error, (candidate) => {
7023
+ const details = getSandboxErrorDetails(candidate);
7024
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7025
+ return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7026
+ });
7027
+ }
7028
+ function wrapSandboxSetupError(error) {
7029
+ try {
7030
+ const details = getSandboxErrorDetails(error);
7031
+ if (details.summary) {
7032
+ return new Error(`sandbox setup failed (${details.summary})`, {
7033
+ cause: error
7034
+ });
7035
+ }
7036
+ } catch {
7037
+ }
7038
+ let causeMessage;
7039
+ try {
7040
+ causeMessage = getFirstErrorMessage(error);
7041
+ } catch (cause) {
7042
+ causeMessage = cause instanceof Error ? cause.message : void 0;
7043
+ }
7044
+ if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7045
+ const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7046
+ return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7047
+ }
7048
+ return new Error("sandbox setup failed", { cause: error });
7049
+ }
7050
+ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7051
+ const details = getSandboxErrorDetails(error);
7052
+ setSpanAttributes({
7053
+ ...details.attributes,
7054
+ ...includeMissingPath ? {
7055
+ "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7056
+ } : {},
7057
+ "app.sandbox.success": false
7058
+ });
7059
+ setSpanStatus("error");
7060
+ throw new Error(
7061
+ details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7062
+ {
7063
+ cause: error
7064
+ }
7065
+ );
7066
+ }
7067
+
7068
+ // src/chat/sandbox/session.ts
7069
+ import { Sandbox } from "@vercel/sandbox";
7070
+ import { createBashTool as createBashTool2 } from "bash-tool";
7071
+
6976
7072
  // src/chat/runtime/status-format.ts
6977
7073
  var SLACK_STATUS_MAX_LENGTH = 50;
6978
7074
  function truncateWithEllipsis(text, maxLength) {
@@ -7234,112 +7330,11 @@ function logAssistantStatusFailure(status, error) {
7234
7330
  );
7235
7331
  }
7236
7332
 
7237
- // src/chat/sandbox/sandbox.ts
7238
- var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
7239
- var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
7240
- var SANDBOX_RUNTIME = "node22";
7241
- var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
7242
- var EVAL_GH_STUB_PATH = `${SANDBOX_RUNTIME_BIN_DIR}/gh`;
7243
- var SNAPSHOT_BOOT_RETRY_COUNT = 3;
7244
- var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
7245
- var SANDBOX_ERROR_FIELDS = [
7246
- {
7247
- sourceKey: "sandboxId",
7248
- attributeKey: "sandbox_id",
7249
- summaryKey: "sandboxId"
7250
- }
7251
- ];
7252
- function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
7253
- const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
7254
- const existingAllowRaw = basePolicy.allow;
7255
- const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
7256
- Object.entries(existingAllowRaw).map(
7257
- ([domain, rules]) => [
7258
- domain,
7259
- Array.isArray(rules) ? [...rules] : []
7260
- ]
7261
- )
7262
- ) : { "*": [] };
7263
- for (const transform of headerTransforms) {
7264
- const currentRules = existingAllow[transform.domain] ?? [];
7265
- existingAllow[transform.domain] = [
7266
- ...currentRules,
7267
- { transform: [{ headers: transform.headers }] }
7268
- ];
7269
- }
7270
- return {
7271
- ...basePolicy,
7272
- allow: existingAllow
7273
- };
7274
- }
7275
- function truncateOutput(output, maxLength) {
7276
- if (output.length <= maxLength) {
7277
- return { value: output, truncated: false };
7278
- }
7279
- const truncatedLength = output.length - maxLength;
7280
- return {
7281
- value: `${output.slice(0, maxLength)}
7333
+ // src/chat/sandbox/skill-sync.ts
7334
+ import fs3 from "fs/promises";
7335
+ import path4 from "path";
7282
7336
 
7283
- [output truncated: ${truncatedLength} characters removed]`,
7284
- truncated: true
7285
- };
7286
- }
7287
- function toPosixRelative(base, absolute) {
7288
- return path4.relative(base, absolute).split(path4.sep).join("/");
7289
- }
7290
- async function listFilesRecursive(root) {
7291
- const queue = [root];
7292
- const files = [];
7293
- while (queue.length > 0) {
7294
- const dir = queue.shift();
7295
- const entries = await fs3.readdir(dir, { withFileTypes: true });
7296
- entries.sort((a, b) => a.name.localeCompare(b.name));
7297
- for (const entry of entries) {
7298
- const absolute = path4.join(dir, entry.name);
7299
- if (entry.isDirectory()) {
7300
- queue.push(absolute);
7301
- } else if (entry.isFile()) {
7302
- files.push(absolute);
7303
- }
7304
- }
7305
- }
7306
- return files;
7307
- }
7308
- async function buildSkillSyncFiles(availableSkills) {
7309
- const filesToWrite = [];
7310
- const index = {
7311
- skills: []
7312
- };
7313
- for (const skill of availableSkills) {
7314
- const skillFiles = await listFilesRecursive(skill.skillPath);
7315
- for (const absoluteFile of skillFiles) {
7316
- const relative = toPosixRelative(skill.skillPath, absoluteFile);
7317
- if (!relative || relative.startsWith("..")) {
7318
- continue;
7319
- }
7320
- filesToWrite.push({
7321
- path: `${sandboxSkillDir(skill.name)}/${relative}`,
7322
- content: await fs3.readFile(absoluteFile)
7323
- });
7324
- }
7325
- index.skills.push({
7326
- name: skill.name,
7327
- description: skill.description,
7328
- root: sandboxSkillDir(skill.name)
7329
- });
7330
- }
7331
- filesToWrite.push({
7332
- path: `${SANDBOX_SKILLS_ROOT}/index.json`,
7333
- content: Buffer.from(JSON.stringify(index), "utf8")
7334
- });
7335
- if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
7336
- filesToWrite.push({
7337
- path: EVAL_GH_STUB_PATH,
7338
- content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
7339
- });
7340
- }
7341
- return filesToWrite;
7342
- }
7337
+ // src/chat/sandbox/eval-gh-stub.ts
7343
7338
  function buildEvalGitHubCliStub() {
7344
7339
  return `#!/usr/bin/env node
7345
7340
  const fs = require("node:fs");
@@ -7517,6 +7512,8 @@ if (args[0] === "api") {
7517
7512
  outputJson({ items: [] });
7518
7513
  process.exit(0);
7519
7514
  }
7515
+ outputJson({});
7516
+ process.exit(0);
7520
7517
  }
7521
7518
 
7522
7519
  if (args[0] === "issue") {
@@ -7558,7 +7555,9 @@ if (args[0] === "issue") {
7558
7555
 
7559
7556
  const number = Number.parseInt(positionals[2] || "", 10);
7560
7557
  const key = repo + "#" + number;
7561
- const record = state.issues[key] || defaultIssue(repo, Number.isFinite(number) ? number : 101);
7558
+ const record =
7559
+ state.issues[key] ||
7560
+ defaultIssue(repo, Number.isFinite(number) ? number : 101);
7562
7561
 
7563
7562
  if (subcommand === "view") {
7564
7563
  const jsonFields = getFlag("--json");
@@ -7599,156 +7598,315 @@ if (args[0] === "issue") {
7599
7598
  fallbackToRealGh();
7600
7599
  `;
7601
7600
  }
7602
- function collectDirectories(filesToWrite) {
7603
- const directoriesToEnsure = /* @__PURE__ */ new Set();
7604
- for (const file of filesToWrite) {
7605
- const normalizedPath = path4.posix.normalize(file.path);
7606
- const parts = normalizedPath.split("/").filter(Boolean);
7607
- let current = "";
7608
- for (let index = 0; index < parts.length - 1; index += 1) {
7609
- current = `${current}/${parts[index]}`;
7610
- directoriesToEnsure.add(current);
7601
+
7602
+ // src/chat/sandbox/skill-sync.ts
7603
+ function toPosixRelative(base, absolute) {
7604
+ return path4.relative(base, absolute).split(path4.sep).join("/");
7605
+ }
7606
+ async function listFilesRecursive(root) {
7607
+ const queue = [root];
7608
+ const files = [];
7609
+ while (queue.length > 0) {
7610
+ const dir = queue.shift();
7611
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
7612
+ entries.sort((a, b) => a.name.localeCompare(b.name));
7613
+ for (const entry of entries) {
7614
+ const absolute = path4.join(dir, entry.name);
7615
+ if (entry.isDirectory()) {
7616
+ queue.push(absolute);
7617
+ } else if (entry.isFile()) {
7618
+ files.push(absolute);
7619
+ }
7611
7620
  }
7612
7621
  }
7613
- return Array.from(directoriesToEnsure).filter(
7614
- (directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
7615
- ).sort((a, b) => a.length - b.length);
7616
- }
7617
- function getSandboxErrorDetails(error) {
7618
- return extractHttpErrorDetails(error, {
7619
- attributePrefix: "app.sandbox.api_error",
7620
- extraFields: [...SANDBOX_ERROR_FIELDS]
7621
- });
7622
- }
7623
- function sleep2(ms) {
7624
- return new Promise((resolve) => {
7625
- setTimeout(resolve, ms);
7626
- });
7627
- }
7628
- function isAlreadyExistsError(error) {
7629
- const details = getSandboxErrorDetails(error);
7630
- return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7622
+ return files;
7631
7623
  }
7632
- function findInErrorChain(error, predicate) {
7633
- const seen = /* @__PURE__ */ new Set();
7634
- let current = error;
7635
- while (current && !seen.has(current)) {
7636
- if (predicate(current)) {
7637
- return true;
7638
- }
7639
- seen.add(current);
7640
- if (typeof current === "object") {
7641
- current = current.cause;
7642
- } else {
7643
- current = void 0;
7624
+ async function buildSkillSyncFiles(availableSkills, runtimeBinDir) {
7625
+ const filesToWrite = [];
7626
+ const index = {
7627
+ skills: []
7628
+ };
7629
+ for (const skill of availableSkills) {
7630
+ const skillFiles = await listFilesRecursive(skill.skillPath);
7631
+ for (const absoluteFile of skillFiles) {
7632
+ const relative = toPosixRelative(skill.skillPath, absoluteFile);
7633
+ if (!relative || relative.startsWith("..")) {
7634
+ continue;
7635
+ }
7636
+ filesToWrite.push({
7637
+ path: `${sandboxSkillDir(skill.name)}/${relative}`,
7638
+ content: await fs3.readFile(absoluteFile)
7639
+ });
7644
7640
  }
7641
+ index.skills.push({
7642
+ name: skill.name,
7643
+ description: skill.description,
7644
+ root: sandboxSkillDir(skill.name)
7645
+ });
7645
7646
  }
7646
- return false;
7647
- }
7648
- function isSandboxUnavailableError(error) {
7649
- return findInErrorChain(error, (candidate) => {
7650
- const details = getSandboxErrorDetails(candidate);
7651
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7652
- return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7653
- });
7654
- }
7655
- function isSnapshottingError(error) {
7656
- return findInErrorChain(error, (candidate) => {
7657
- const details = getSandboxErrorDetails(candidate);
7658
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7659
- return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7647
+ filesToWrite.push({
7648
+ path: `${SANDBOX_SKILLS_ROOT}/index.json`,
7649
+ content: Buffer.from(JSON.stringify(index), "utf8")
7660
7650
  });
7651
+ if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
7652
+ filesToWrite.push({
7653
+ path: `${runtimeBinDir}/gh`,
7654
+ content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
7655
+ });
7656
+ }
7657
+ return filesToWrite;
7661
7658
  }
7662
- function getFirstErrorMessage(error) {
7663
- const seen = /* @__PURE__ */ new Set();
7664
- let current = error;
7665
- while (current && !seen.has(current)) {
7666
- if (current instanceof Error) {
7667
- const message = current.message.trim();
7668
- if (message) {
7669
- return message;
7670
- }
7659
+ function collectDirectories(filesToWrite, workspaceRoot) {
7660
+ const directoriesToEnsure = /* @__PURE__ */ new Set();
7661
+ for (const file of filesToWrite) {
7662
+ const normalizedPath = path4.posix.normalize(file.path);
7663
+ const parts = normalizedPath.split("/").filter(Boolean);
7664
+ let current = "";
7665
+ for (let index = 0; index < parts.length - 1; index += 1) {
7666
+ current = `${current}/${parts[index]}`;
7667
+ directoriesToEnsure.add(current);
7671
7668
  }
7672
- seen.add(current);
7673
- current = typeof current === "object" ? current.cause : void 0;
7674
7669
  }
7675
- return void 0;
7670
+ return Array.from(directoriesToEnsure).filter(
7671
+ (directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
7672
+ ).sort((a, b) => a.length - b.length);
7676
7673
  }
7677
- function wrapSandboxSetupError(error) {
7678
- try {
7679
- const details = getSandboxErrorDetails(error);
7680
- if (details.summary) {
7681
- return new Error(`sandbox setup failed (${details.summary})`, {
7682
- cause: error
7683
- });
7674
+ function resolveHostSkillPath(availableSkills, sandboxPath) {
7675
+ const normalizedPath = path4.posix.normalize(sandboxPath.trim());
7676
+ for (const skill of availableSkills) {
7677
+ const virtualRoot = sandboxSkillDir(skill.name);
7678
+ if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
7679
+ continue;
7684
7680
  }
7685
- } catch {
7681
+ const relativePath = path4.posix.relative(virtualRoot, normalizedPath);
7682
+ if (!relativePath || relativePath.startsWith("../")) {
7683
+ return null;
7684
+ }
7685
+ const hostRoot = path4.resolve(skill.skillPath);
7686
+ const hostPath = path4.resolve(hostRoot, ...relativePath.split("/"));
7687
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path4.sep}`)) {
7688
+ return null;
7689
+ }
7690
+ return hostPath;
7686
7691
  }
7687
- let causeMessage;
7688
- try {
7689
- causeMessage = getFirstErrorMessage(error);
7690
- } catch (cause) {
7691
- causeMessage = cause instanceof Error ? cause.message : void 0;
7692
+ return null;
7693
+ }
7694
+ function isHostFileMissingError(error) {
7695
+ return Boolean(
7696
+ error && typeof error === "object" && error.code === "ENOENT"
7697
+ );
7698
+ }
7699
+ async function syncSkillsToSandbox(params) {
7700
+ const workspaceRoot = params.workspaceRoot ?? SANDBOX_WORKSPACE_ROOT;
7701
+ await params.withSpan(
7702
+ "sandbox.sync_skills",
7703
+ "sandbox.sync",
7704
+ {
7705
+ "app.sandbox.skills_count": params.skills.length
7706
+ },
7707
+ async () => {
7708
+ const filesToWrite = await buildSkillSyncFiles(
7709
+ params.skills,
7710
+ params.runtimeBinDir
7711
+ );
7712
+ const bytesWritten = filesToWrite.reduce(
7713
+ (total, file) => total + file.content.length,
7714
+ 0
7715
+ );
7716
+ const directories = collectDirectories(filesToWrite, workspaceRoot);
7717
+ await params.withSpan(
7718
+ "sandbox.sync_writeFiles",
7719
+ "sandbox.sync.write",
7720
+ {
7721
+ "app.sandbox.sync.files_written": filesToWrite.length,
7722
+ "app.sandbox.sync.bytes_written": bytesWritten,
7723
+ "app.sandbox.sync.directories_ensured": directories.length
7724
+ },
7725
+ async () => {
7726
+ try {
7727
+ for (const directory of directories) {
7728
+ try {
7729
+ await params.sandbox.mkDir(directory);
7730
+ } catch (error) {
7731
+ if (!isAlreadyExistsError(error)) {
7732
+ throw error;
7733
+ }
7734
+ }
7735
+ }
7736
+ await params.sandbox.writeFiles(filesToWrite);
7737
+ const executableFiles = filesToWrite.map((file) => file.path).filter(
7738
+ (filePath) => filePath.startsWith(`${params.runtimeBinDir}/`)
7739
+ );
7740
+ for (const filePath of executableFiles) {
7741
+ const chmod = await runNonInteractiveCommand(params.sandbox, {
7742
+ cmd: "chmod",
7743
+ args: ["0755", filePath],
7744
+ cwd: workspaceRoot
7745
+ });
7746
+ if (chmod.exitCode !== 0) {
7747
+ throw new Error(
7748
+ `sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
7749
+ );
7750
+ }
7751
+ }
7752
+ } catch (error) {
7753
+ throwSandboxOperationError("sandbox writeFiles", error, true);
7754
+ }
7755
+ }
7756
+ );
7757
+ }
7758
+ );
7759
+ }
7760
+
7761
+ // src/chat/sandbox/session.ts
7762
+ var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
7763
+ var SANDBOX_RUNTIME = "node22";
7764
+ var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
7765
+ var SNAPSHOT_BOOT_RETRY_COUNT = 3;
7766
+ var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
7767
+ var SNAPSHOT_PHASE_STATUS = {
7768
+ resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
7769
+ waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
7770
+ building_snapshot: { kind: "creating", context: "sandbox snapshot" },
7771
+ cache_hit: { kind: "loading", context: "sandbox snapshot" }
7772
+ };
7773
+ function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
7774
+ const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
7775
+ const existingAllowRaw = basePolicy.allow;
7776
+ const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
7777
+ Object.entries(existingAllowRaw).map(
7778
+ ([domain, rules]) => [
7779
+ domain,
7780
+ Array.isArray(rules) ? [...rules] : []
7781
+ ]
7782
+ )
7783
+ ) : { "*": [] };
7784
+ for (const transform of headerTransforms) {
7785
+ const currentRules = existingAllow[transform.domain] ?? [];
7786
+ existingAllow[transform.domain] = [
7787
+ ...currentRules,
7788
+ { transform: [{ headers: transform.headers }] }
7789
+ ];
7692
7790
  }
7693
- if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7694
- const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7695
- return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7791
+ return {
7792
+ ...basePolicy,
7793
+ allow: existingAllow
7794
+ };
7795
+ }
7796
+ function truncateOutput(output, maxLength) {
7797
+ if (output.length <= maxLength) {
7798
+ return { value: output, truncated: false };
7696
7799
  }
7697
- return new Error("sandbox setup failed", { cause: error });
7800
+ const truncatedLength = output.length - maxLength;
7801
+ return {
7802
+ value: `${output.slice(0, maxLength)}
7803
+
7804
+ [output truncated: ${truncatedLength} characters removed]`,
7805
+ truncated: true
7806
+ };
7698
7807
  }
7699
- function throwSandboxOperationError(action, error, includeMissingPath = false) {
7700
- const details = getSandboxErrorDetails(error);
7701
- setSpanAttributes({
7702
- ...details.attributes,
7703
- ...includeMissingPath ? {
7704
- "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7705
- } : {},
7706
- "app.sandbox.success": false
7808
+ function sleep2(ms) {
7809
+ return new Promise((resolve) => {
7810
+ setTimeout(resolve, ms);
7707
7811
  });
7708
- setSpanStatus("error");
7709
- throw new Error(
7710
- details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7711
- {
7712
- cause: error
7812
+ }
7813
+ function createStatusEmitter(emitStatus) {
7814
+ let statusCount = 0;
7815
+ const sentStatuses = /* @__PURE__ */ new Set();
7816
+ const emit = async (status) => {
7817
+ const statusKey = `${status.kind}:${status.context ?? ""}`;
7818
+ if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
7819
+ return;
7820
+ }
7821
+ sentStatuses.add(statusKey);
7822
+ statusCount += 1;
7823
+ await emitStatus(status);
7824
+ };
7825
+ const reportSnapshotPhase = async (phase) => {
7826
+ const status = SNAPSHOT_PHASE_STATUS[phase];
7827
+ if (status) {
7828
+ await emit(makeAssistantStatus(status.kind, status.context));
7713
7829
  }
7830
+ };
7831
+ return { emit, reportSnapshotPhase };
7832
+ }
7833
+ function parseKeepAliveMs() {
7834
+ const parsed = Number.parseInt(
7835
+ process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
7836
+ 10
7714
7837
  );
7838
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
7715
7839
  }
7716
- function createSandboxExecutor(options) {
7840
+ function createSandboxSessionManager(options) {
7717
7841
  let sandbox = null;
7718
7842
  let sandboxIdHint = options?.sandboxId;
7719
7843
  let availableSkills = [];
7720
7844
  let toolExecutors;
7721
7845
  const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
7722
7846
  const traceContext = options?.traceContext ?? {};
7723
- const emitStatus = options?.onStatus;
7724
7847
  const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
7725
7848
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
7726
- const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, onStatus) => {
7727
- for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
7728
- try {
7729
- await onStatus?.(makeAssistantStatus("loading", "sandbox"));
7730
- return await Sandbox.create({
7731
- timeout: timeoutMs,
7732
- source: {
7733
- type: "snapshot",
7734
- snapshotId
7735
- },
7736
- ...sandboxCredentials ?? {}
7737
- });
7738
- } catch (error) {
7739
- if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
7740
- throw error;
7849
+ const emitSandboxStatus = async (source, statusEmitter, status) => {
7850
+ logInfo(
7851
+ "sandbox_status_emitted",
7852
+ traceContext,
7853
+ {
7854
+ "app.sandbox.status.source": source,
7855
+ "app.sandbox.status.kind": status.kind,
7856
+ ...status.context ? { "app.sandbox.status.context": status.context } : {}
7857
+ },
7858
+ "Sandbox status emitted"
7859
+ );
7860
+ if (typeof statusEmitter === "function") {
7861
+ await statusEmitter(status);
7862
+ return;
7863
+ }
7864
+ await statusEmitter.emit(status);
7865
+ };
7866
+ const clearSession = () => {
7867
+ sandbox = null;
7868
+ sandboxIdHint = void 0;
7869
+ toolExecutors = void 0;
7870
+ };
7871
+ const rememberSandbox = (nextSandbox) => {
7872
+ sandbox = nextSandbox;
7873
+ sandboxIdHint = nextSandbox.sandboxId;
7874
+ toolExecutors = void 0;
7875
+ return nextSandbox;
7876
+ };
7877
+ const failSetup = (error) => {
7878
+ throw wrapSandboxSetupError(error);
7879
+ };
7880
+ const syncSkills = async (targetSandbox) => {
7881
+ await syncSkillsToSandbox({
7882
+ sandbox: targetSandbox,
7883
+ skills: availableSkills,
7884
+ withSpan: withSandboxSpan,
7885
+ runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
7886
+ });
7887
+ };
7888
+ const ensureSandboxReachable = async (targetSandbox, source) => {
7889
+ await withSandboxSpan(
7890
+ "sandbox.reuse_probe",
7891
+ "sandbox.acquire.probe",
7892
+ {
7893
+ "app.sandbox.reused": true,
7894
+ "app.sandbox.source": source
7895
+ },
7896
+ async () => {
7897
+ try {
7898
+ await targetSandbox.mkDir(SANDBOX_WORKSPACE_ROOT);
7899
+ } catch (error) {
7900
+ if (!isAlreadyExistsError(error)) {
7901
+ throw error;
7902
+ }
7741
7903
  }
7742
- await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
7743
7904
  }
7744
- }
7745
- throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
7905
+ );
7746
7906
  };
7747
7907
  const invalidateSandboxInstance = async (targetSandbox, reason) => {
7748
7908
  if (sandbox === targetSandbox) {
7749
- sandbox = null;
7750
- sandboxIdHint = void 0;
7751
- toolExecutors = void 0;
7909
+ clearSession();
7752
7910
  }
7753
7911
  logWarn(
7754
7912
  "sandbox_network_policy_restore_failed",
@@ -7763,290 +7921,304 @@ function createSandboxExecutor(options) {
7763
7921
  } catch {
7764
7922
  }
7765
7923
  };
7766
- const upsertSkillsToSandbox = async (targetSandbox) => {
7767
- await withSandboxSpan(
7768
- "sandbox.sync_skills",
7769
- "sandbox.sync",
7770
- {
7771
- "app.sandbox.skills_count": availableSkills.length
7772
- },
7773
- async () => {
7774
- const filesToWrite = await buildSkillSyncFiles(availableSkills);
7775
- const bytesWritten = filesToWrite.reduce(
7776
- (total, file) => total + file.content.length,
7777
- 0
7778
- );
7779
- const directories = collectDirectories(filesToWrite);
7780
- await withSandboxSpan(
7781
- "sandbox.sync_writeFiles",
7782
- "sandbox.sync.write",
7783
- {
7784
- "app.sandbox.sync.files_written": filesToWrite.length,
7785
- "app.sandbox.sync.bytes_written": bytesWritten,
7786
- "app.sandbox.sync.directories_ensured": directories.length
7924
+ const recreateUnavailableSandbox = async (source) => {
7925
+ setSpanAttributes({
7926
+ "app.sandbox.recovery.attempted": true,
7927
+ "app.sandbox.recovery.source": source
7928
+ });
7929
+ clearSession();
7930
+ const replacement = await createFreshSandbox();
7931
+ setSpanAttributes({
7932
+ "app.sandbox.recovery.succeeded": true
7933
+ });
7934
+ return replacement;
7935
+ };
7936
+ const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
7937
+ for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
7938
+ try {
7939
+ if (emitStatus) {
7940
+ await emitSandboxStatus(
7941
+ "snapshot_boot",
7942
+ emitStatus,
7943
+ makeAssistantStatus("loading", "sandbox")
7944
+ );
7945
+ }
7946
+ return await Sandbox.create({
7947
+ timeout: timeoutMs,
7948
+ source: {
7949
+ type: "snapshot",
7950
+ snapshotId
7787
7951
  },
7788
- async () => {
7789
- try {
7790
- for (const directory of directories) {
7791
- try {
7792
- await targetSandbox.mkDir(directory);
7793
- } catch (error) {
7794
- if (!isAlreadyExistsError(error)) {
7795
- throw error;
7796
- }
7797
- }
7798
- }
7799
- await targetSandbox.writeFiles(filesToWrite);
7800
- const executableFiles = filesToWrite.map((file) => file.path).filter(
7801
- (filePath) => filePath.startsWith(`${SANDBOX_RUNTIME_BIN_DIR}/`)
7802
- );
7803
- for (const filePath of executableFiles) {
7804
- const chmod = await runNonInteractiveCommand(targetSandbox, {
7805
- cmd: "chmod",
7806
- args: ["0755", filePath],
7807
- cwd: SANDBOX_WORKSPACE_ROOT
7808
- });
7809
- if (chmod.exitCode !== 0) {
7810
- throw new Error(
7811
- `sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
7812
- );
7813
- }
7814
- }
7815
- } catch (error) {
7816
- throwSandboxOperationError("sandbox writeFiles", error, true);
7817
- }
7818
- }
7819
- );
7952
+ ...sandboxCredentials ?? {}
7953
+ });
7954
+ } catch (error) {
7955
+ if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
7956
+ throw error;
7957
+ }
7958
+ await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
7820
7959
  }
7821
- );
7960
+ }
7961
+ throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
7962
+ };
7963
+ const setSnapshotAttributes = (snapshot) => {
7964
+ setSpanAttributes({
7965
+ "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
7966
+ "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
7967
+ "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
7968
+ ...snapshot.profileHash ? {
7969
+ "app.sandbox.snapshot.profile_hash": snapshot.profileHash
7970
+ } : {},
7971
+ "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
7972
+ ...snapshot.rebuildReason ? {
7973
+ "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
7974
+ } : {}
7975
+ });
7976
+ };
7977
+ const createSandboxFromResolvedSnapshot = async (params) => {
7978
+ const { runtime, snapshot, sandboxCredentials, status } = params;
7979
+ if (!snapshot.snapshotId) {
7980
+ await emitSandboxStatus(
7981
+ "fresh_runtime_boot",
7982
+ status,
7983
+ makeAssistantStatus("loading", "sandbox")
7984
+ );
7985
+ return await Sandbox.create({
7986
+ timeout: timeoutMs,
7987
+ runtime,
7988
+ ...sandboxCredentials ?? {}
7989
+ });
7990
+ }
7991
+ try {
7992
+ return await createSandboxFromSnapshot(
7993
+ snapshot.snapshotId,
7994
+ sandboxCredentials,
7995
+ status.emit
7996
+ );
7997
+ } catch (error) {
7998
+ if (!isSnapshotMissingError(error)) {
7999
+ throw error;
8000
+ }
8001
+ setSpanAttributes({
8002
+ "app.sandbox.snapshot.rebuild_after_missing": true
8003
+ });
8004
+ const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
8005
+ runtime,
8006
+ timeoutMs,
8007
+ forceRebuild: true,
8008
+ staleSnapshotId: snapshot.snapshotId,
8009
+ onProgress: status.reportSnapshotPhase
8010
+ });
8011
+ if (!rebuiltSnapshot.snapshotId) {
8012
+ throw error;
8013
+ }
8014
+ return await createSandboxFromSnapshot(
8015
+ rebuiltSnapshot.snapshotId,
8016
+ sandboxCredentials,
8017
+ status.emit
8018
+ );
8019
+ }
8020
+ };
8021
+ const createFreshSandbox = async () => {
8022
+ const runtime = SANDBOX_RUNTIME;
8023
+ const sandboxCredentials = getVercelSandboxCredentials();
8024
+ const status = createStatusEmitter(options?.onStatus);
8025
+ let createdSandbox;
8026
+ try {
8027
+ createdSandbox = await withSandboxSpan(
8028
+ "sandbox.create",
8029
+ "sandbox.create",
8030
+ {
8031
+ "app.sandbox.reused": false,
8032
+ "app.sandbox.timeout_ms": timeoutMs,
8033
+ "app.sandbox.runtime": runtime
8034
+ },
8035
+ async () => {
8036
+ await emitSandboxStatus(
8037
+ "runtime_dependency_resolve",
8038
+ status,
8039
+ makeAssistantStatus("loading", "sandbox runtime")
8040
+ );
8041
+ const snapshot = await resolveRuntimeDependencySnapshot({
8042
+ runtime,
8043
+ timeoutMs,
8044
+ onProgress: status.reportSnapshotPhase
8045
+ });
8046
+ setSnapshotAttributes(snapshot);
8047
+ return await createSandboxFromResolvedSnapshot({
8048
+ runtime,
8049
+ snapshot,
8050
+ sandboxCredentials,
8051
+ status
8052
+ });
8053
+ }
8054
+ );
8055
+ } catch (error) {
8056
+ return failSetup(error);
8057
+ }
8058
+ try {
8059
+ await syncSkills(createdSandbox);
8060
+ } catch (error) {
8061
+ return failSetup(error);
8062
+ }
8063
+ return rememberSandbox(createdSandbox);
8064
+ };
8065
+ const discardHintIfProfileChanged = () => {
8066
+ if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
8067
+ return;
8068
+ }
8069
+ setSpanAttributes({
8070
+ "app.sandbox.reused": false,
8071
+ "app.sandbox.recreate.reason": "dependency_profile_mismatch",
8072
+ ...options?.sandboxDependencyProfileHash ? {
8073
+ "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
8074
+ } : {},
8075
+ ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8076
+ });
8077
+ sandboxIdHint = void 0;
8078
+ };
8079
+ const tryReuseCachedSandbox = async () => {
8080
+ const cachedSandbox = sandbox;
8081
+ if (!cachedSandbox) {
8082
+ return null;
8083
+ }
8084
+ try {
8085
+ await ensureSandboxReachable(cachedSandbox, "memory");
8086
+ return cachedSandbox;
8087
+ } catch (error) {
8088
+ if (isSandboxUnavailableError(error)) {
8089
+ return await recreateUnavailableSandbox("memory");
8090
+ }
8091
+ return failSetup(error);
8092
+ }
8093
+ };
8094
+ const tryRestoreHintedSandbox = async () => {
8095
+ if (!sandboxIdHint) {
8096
+ return null;
8097
+ }
8098
+ let hintedSandbox = null;
8099
+ try {
8100
+ const sandboxCredentials = getVercelSandboxCredentials();
8101
+ hintedSandbox = await withSandboxSpan(
8102
+ "sandbox.get",
8103
+ "sandbox.get",
8104
+ {
8105
+ "app.sandbox.reused": true,
8106
+ "app.sandbox.source": "id_hint"
8107
+ },
8108
+ async () => await Sandbox.get({
8109
+ sandboxId: sandboxIdHint,
8110
+ ...sandboxCredentials ?? {}
8111
+ })
8112
+ );
8113
+ } catch {
8114
+ return null;
8115
+ }
8116
+ try {
8117
+ await syncSkills(hintedSandbox);
8118
+ return rememberSandbox(hintedSandbox);
8119
+ } catch (error) {
8120
+ if (isSandboxUnavailableError(error)) {
8121
+ return await recreateUnavailableSandbox("id_hint");
8122
+ }
8123
+ return failSetup(error);
8124
+ }
7822
8125
  };
7823
8126
  const acquireSandbox = async () => {
7824
- return withSandboxSpan(
8127
+ return await withSandboxSpan(
7825
8128
  "sandbox.acquire",
7826
8129
  "sandbox.acquire",
7827
8130
  {
7828
8131
  "app.sandbox.id_hint_present": Boolean(sandboxIdHint),
7829
8132
  "app.sandbox.timeout_ms": timeoutMs,
7830
- "app.sandbox.runtime": "node22",
8133
+ "app.sandbox.runtime": SANDBOX_RUNTIME,
7831
8134
  "app.sandbox.skills_count": availableSkills.length
7832
8135
  },
7833
8136
  async () => {
7834
- const sandboxCredentials = getVercelSandboxCredentials();
7835
- const assignSandbox = (nextSandbox) => {
7836
- sandbox = nextSandbox;
7837
- sandboxIdHint = nextSandbox.sandboxId;
7838
- toolExecutors = void 0;
7839
- return nextSandbox;
7840
- };
7841
- const handleSetupFailure = (error) => {
7842
- throw wrapSandboxSetupError(error);
7843
- };
7844
- const createFreshSandbox = async () => {
7845
- const runtime = SANDBOX_RUNTIME;
7846
- let statusCount = 0;
7847
- const sentStatuses = /* @__PURE__ */ new Set();
7848
- const emitSandboxStatus = async (status) => {
7849
- const statusKey = `${status.kind}:${status.context ?? ""}`;
7850
- if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
7851
- return;
7852
- }
7853
- sentStatuses.add(statusKey);
7854
- statusCount += 1;
7855
- await emitStatus(status);
7856
- };
7857
- const reportSnapshotPhase = async (phase) => {
7858
- if (phase === "resolve_start") {
7859
- await emitSandboxStatus(
7860
- makeAssistantStatus("loading", "sandbox snapshot cache")
7861
- );
7862
- return;
7863
- }
7864
- if (phase === "waiting_for_lock") {
7865
- await emitSandboxStatus(
7866
- makeAssistantStatus("loading", "sandbox snapshot build")
7867
- );
7868
- return;
7869
- }
7870
- if (phase === "building_snapshot") {
7871
- await emitSandboxStatus(
7872
- makeAssistantStatus("creating", "sandbox snapshot")
7873
- );
7874
- return;
7875
- }
7876
- if (phase === "cache_hit") {
7877
- await emitSandboxStatus(
7878
- makeAssistantStatus("loading", "sandbox snapshot")
7879
- );
7880
- }
7881
- };
7882
- let createdSandbox;
7883
- try {
7884
- createdSandbox = await withSandboxSpan(
7885
- "sandbox.create",
7886
- "sandbox.create",
7887
- {
7888
- "app.sandbox.reused": false,
7889
- "app.sandbox.timeout_ms": timeoutMs,
7890
- "app.sandbox.runtime": runtime
7891
- },
7892
- async () => {
7893
- await emitSandboxStatus(
7894
- makeAssistantStatus("loading", "sandbox runtime")
7895
- );
7896
- const snapshot = await resolveRuntimeDependencySnapshot({
7897
- runtime,
7898
- timeoutMs,
7899
- onProgress: reportSnapshotPhase
7900
- });
7901
- setSpanAttributes({
7902
- "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
7903
- "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
7904
- "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
7905
- ...snapshot.profileHash ? {
7906
- "app.sandbox.snapshot.profile_hash": snapshot.profileHash
7907
- } : {},
7908
- "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
7909
- ...snapshot.rebuildReason ? {
7910
- "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
7911
- } : {}
7912
- });
7913
- if (!snapshot.snapshotId) {
7914
- await emitSandboxStatus(
7915
- makeAssistantStatus("loading", "sandbox")
7916
- );
7917
- return await Sandbox.create({
7918
- timeout: timeoutMs,
7919
- runtime,
7920
- ...sandboxCredentials ?? {}
7921
- });
7922
- }
7923
- try {
7924
- return await createSandboxFromSnapshot(
7925
- snapshot.snapshotId,
7926
- sandboxCredentials,
7927
- emitSandboxStatus
7928
- );
7929
- } catch (error) {
7930
- if (!isSnapshotMissingError(error)) {
7931
- throw error;
7932
- }
7933
- setSpanAttributes({
7934
- "app.sandbox.snapshot.rebuild_after_missing": true
7935
- });
7936
- const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
7937
- runtime,
7938
- timeoutMs,
7939
- forceRebuild: true,
7940
- staleSnapshotId: snapshot.snapshotId,
7941
- onProgress: reportSnapshotPhase
7942
- });
7943
- if (!rebuiltSnapshot.snapshotId) {
7944
- throw error;
7945
- }
7946
- return await createSandboxFromSnapshot(
7947
- rebuiltSnapshot.snapshotId,
7948
- sandboxCredentials,
7949
- emitSandboxStatus
7950
- );
7951
- }
7952
- }
7953
- );
7954
- } catch (error) {
7955
- return handleSetupFailure(error);
7956
- }
7957
- try {
7958
- await upsertSkillsToSandbox(createdSandbox);
7959
- } catch (error) {
7960
- return handleSetupFailure(error);
7961
- }
7962
- return assignSandbox(createdSandbox);
7963
- };
7964
- if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
7965
- setSpanAttributes({
7966
- "app.sandbox.reused": false,
7967
- "app.sandbox.recreate.reason": "dependency_profile_mismatch",
7968
- ...options?.sandboxDependencyProfileHash ? {
7969
- "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
7970
- } : {},
7971
- ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
7972
- });
7973
- sandboxIdHint = void 0;
7974
- }
7975
- const recoverUnavailableSandbox = async (source) => {
7976
- setSpanAttributes({
7977
- "app.sandbox.recovery.attempted": true,
7978
- "app.sandbox.recovery.source": source
7979
- });
7980
- sandbox = null;
7981
- sandboxIdHint = void 0;
7982
- toolExecutors = void 0;
7983
- const replacement = await createFreshSandbox();
7984
- setSpanAttributes({
7985
- "app.sandbox.recovery.succeeded": true
7986
- });
7987
- return replacement;
7988
- };
7989
- if (sandbox) {
7990
- const cachedSandbox = sandbox;
7991
- try {
7992
- await withSandboxSpan(
7993
- "sandbox.reuse_cached",
7994
- "sandbox.acquire.cached",
7995
- {
7996
- "app.sandbox.reused": true,
7997
- "app.sandbox.source": "memory"
7998
- },
7999
- async () => {
8000
- await upsertSkillsToSandbox(cachedSandbox);
8001
- }
8002
- );
8003
- return cachedSandbox;
8004
- } catch (error) {
8005
- if (isSandboxUnavailableError(error)) {
8006
- return recoverUnavailableSandbox("memory");
8007
- }
8008
- return handleSetupFailure(error);
8009
- }
8137
+ discardHintIfProfileChanged();
8138
+ const cachedSandbox = await tryReuseCachedSandbox();
8139
+ if (cachedSandbox) {
8140
+ return cachedSandbox;
8010
8141
  }
8011
- let acquiredSandbox = null;
8012
- if (sandboxIdHint) {
8013
- try {
8014
- acquiredSandbox = await withSandboxSpan(
8015
- "sandbox.get",
8016
- "sandbox.get",
8017
- {
8018
- "app.sandbox.reused": true,
8019
- "app.sandbox.source": "id_hint"
8020
- },
8021
- async () => Sandbox.get({
8022
- sandboxId: sandboxIdHint,
8023
- ...sandboxCredentials ?? {}
8024
- })
8025
- );
8026
- } catch {
8027
- acquiredSandbox = null;
8028
- }
8142
+ const hintedSandbox = await tryRestoreHintedSandbox();
8143
+ if (hintedSandbox) {
8144
+ return hintedSandbox;
8029
8145
  }
8030
- if (acquiredSandbox) {
8031
- try {
8032
- await upsertSkillsToSandbox(acquiredSandbox);
8033
- return assignSandbox(acquiredSandbox);
8034
- } catch (error) {
8035
- if (isSandboxUnavailableError(error)) {
8036
- return recoverUnavailableSandbox("id_hint");
8037
- }
8038
- return handleSetupFailure(error);
8039
- }
8040
- }
8041
- return createFreshSandbox();
8146
+ return await createFreshSandbox();
8042
8147
  }
8043
8148
  );
8044
8149
  };
8045
- const getToolExecutors = async () => {
8046
- if (toolExecutors) {
8047
- return toolExecutors;
8150
+ const getMaxOutputLength = () => {
8151
+ const maxOutputLength = Number.parseInt(
8152
+ process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
8153
+ 10
8154
+ );
8155
+ return Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
8156
+ };
8157
+ const readCommandOutput = async (commandResult2) => {
8158
+ const boundedOutputLength = getMaxOutputLength();
8159
+ const stdoutRaw = await commandResult2.stdout();
8160
+ const stderrRaw = await commandResult2.stderr();
8161
+ const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
8162
+ const stderr = truncateOutput(stderrRaw, boundedOutputLength);
8163
+ return {
8164
+ stdout: stdout.value,
8165
+ stderr: stderr.value,
8166
+ exitCode: commandResult2.exitCode,
8167
+ stdoutTruncated: stdout.truncated,
8168
+ stderrTruncated: stderr.truncated
8169
+ };
8170
+ };
8171
+ const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
8172
+ if (!headerTransforms || headerTransforms.length === 0) {
8173
+ return await callback();
8174
+ }
8175
+ const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
8176
+ const policy = mergeNetworkPolicyWithHeaderTransforms(
8177
+ restoreNetworkPolicy,
8178
+ headerTransforms
8179
+ );
8180
+ await sandboxInstance.updateNetworkPolicy(policy);
8181
+ let callbackError;
8182
+ let restoreError;
8183
+ let result;
8184
+ try {
8185
+ result = await callback();
8186
+ } catch (error) {
8187
+ callbackError = error;
8188
+ throw error;
8189
+ } finally {
8190
+ try {
8191
+ await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
8192
+ } catch (error) {
8193
+ restoreError = error;
8194
+ await invalidateSandboxInstance(sandboxInstance, error);
8195
+ }
8048
8196
  }
8049
- const activeSandbox = await acquireSandbox();
8197
+ if (restoreError && !callbackError) {
8198
+ throw restoreError;
8199
+ }
8200
+ return result;
8201
+ };
8202
+ const extendKeepAlive = async (activeSandbox) => {
8203
+ const keepAliveMs = parseKeepAliveMs();
8204
+ if (keepAliveMs === 0) {
8205
+ return;
8206
+ }
8207
+ try {
8208
+ await withSandboxSpan(
8209
+ "sandbox.keepalive.extend",
8210
+ "sandbox.keepalive",
8211
+ {
8212
+ "app.sandbox.keepalive_ms": keepAliveMs
8213
+ },
8214
+ async () => {
8215
+ await activeSandbox.extendTimeout(keepAliveMs);
8216
+ }
8217
+ );
8218
+ } catch {
8219
+ }
8220
+ };
8221
+ const buildToolExecutors = async (sandboxInstance) => {
8050
8222
  const toolkit = await withSandboxSpan(
8051
8223
  "sandbox.bash_tool.init",
8052
8224
  "sandbox.tool.init",
@@ -8054,8 +8226,8 @@ function createSandboxExecutor(options) {
8054
8226
  "app.sandbox.tool_name": "bash",
8055
8227
  "app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
8056
8228
  },
8057
- async () => createBashTool2({
8058
- sandbox: activeSandbox,
8229
+ async () => await createBashTool2({
8230
+ sandbox: sandboxInstance,
8059
8231
  destination: SANDBOX_WORKSPACE_ROOT
8060
8232
  })
8061
8233
  );
@@ -8064,74 +8236,294 @@ function createSandboxExecutor(options) {
8064
8236
  if (!executeReadFile || !executeWriteFile) {
8065
8237
  throw new Error("bash-tool did not return executable tool handlers");
8066
8238
  }
8067
- toolExecutors = {
8239
+ return {
8068
8240
  bash: async (input) => {
8069
- const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
8070
- const headerTransforms = input.headerTransforms;
8071
- if (headerTransforms && headerTransforms.length > 0) {
8072
- const policy = mergeNetworkPolicyWithHeaderTransforms(
8073
- restoreNetworkPolicy,
8074
- headerTransforms
8075
- );
8076
- await activeSandbox.updateNetworkPolicy(policy);
8077
- }
8078
8241
  const script = buildNonInteractiveShellScript(input.command, {
8079
8242
  env: input.env,
8080
8243
  pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
8081
8244
  });
8082
- let commandError;
8083
- let result;
8084
- let restoreError;
8245
+ return await withTemporaryHeaderTransforms(
8246
+ sandboxInstance,
8247
+ input.headerTransforms,
8248
+ async () => {
8249
+ const commandResult2 = await sandboxInstance.runCommand({
8250
+ cmd: "bash",
8251
+ args: ["-c", script],
8252
+ cwd: SANDBOX_WORKSPACE_ROOT
8253
+ });
8254
+ return await readCommandOutput(commandResult2);
8255
+ }
8256
+ );
8257
+ },
8258
+ readFile: async (input) => await executeReadFile(input, {
8259
+ toolCallId: "sandbox-read-file",
8260
+ messages: []
8261
+ }),
8262
+ writeFile: async (input) => await executeWriteFile(input, {
8263
+ toolCallId: "sandbox-write-file",
8264
+ messages: []
8265
+ })
8266
+ };
8267
+ };
8268
+ const ensureReadySandbox = async () => {
8269
+ const activeSandbox = await acquireSandbox();
8270
+ await extendKeepAlive(activeSandbox);
8271
+ return activeSandbox;
8272
+ };
8273
+ const loadToolExecutors = async (activeSandbox) => {
8274
+ if (toolExecutors) {
8275
+ return toolExecutors;
8276
+ }
8277
+ toolExecutors = await buildToolExecutors(activeSandbox);
8278
+ return toolExecutors;
8279
+ };
8280
+ return {
8281
+ configureSkills(skills) {
8282
+ availableSkills = [...skills];
8283
+ },
8284
+ getSandboxId() {
8285
+ return sandbox?.sandboxId ?? sandboxIdHint;
8286
+ },
8287
+ getDependencyProfileHash() {
8288
+ return dependencyProfileHash;
8289
+ },
8290
+ async createSandbox() {
8291
+ return await acquireSandbox();
8292
+ },
8293
+ async ensureToolExecutors() {
8294
+ return await loadToolExecutors(await ensureReadySandbox());
8295
+ },
8296
+ async dispose() {
8297
+ const activeSandbox = sandbox;
8298
+ if (!activeSandbox) {
8299
+ return;
8300
+ }
8301
+ await withSandboxSpan(
8302
+ "sandbox.stop",
8303
+ "sandbox.stop",
8304
+ {
8305
+ "app.sandbox.stop.blocking": true
8306
+ },
8307
+ async () => {
8308
+ await activeSandbox.stop({ blocking: true });
8309
+ }
8310
+ );
8311
+ sandbox = null;
8312
+ toolExecutors = void 0;
8313
+ }
8314
+ };
8315
+ }
8316
+
8317
+ // src/chat/sandbox/sandbox.ts
8318
+ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
8319
+ function parseHeaderTransforms(raw) {
8320
+ if (!Array.isArray(raw)) {
8321
+ return void 0;
8322
+ }
8323
+ return raw.filter(
8324
+ (value) => Boolean(value && typeof value === "object")
8325
+ ).map((transform) => ({
8326
+ domain: String(transform.domain ?? "").trim(),
8327
+ headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
8328
+ Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8329
+ ) : {}
8330
+ })).filter(
8331
+ (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
8332
+ );
8333
+ }
8334
+ function parseEnv(raw) {
8335
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8336
+ return void 0;
8337
+ }
8338
+ return Object.fromEntries(
8339
+ Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8340
+ );
8341
+ }
8342
+ function createSandboxWorkspace(sandbox) {
8343
+ return {
8344
+ sandboxId: sandbox.sandboxId,
8345
+ readFileToBuffer(input) {
8346
+ return sandbox.readFileToBuffer(input);
8347
+ },
8348
+ runCommand(input) {
8349
+ return sandbox.runCommand(input);
8350
+ }
8351
+ };
8352
+ }
8353
+ function createSandboxExecutor(options) {
8354
+ let availableSkills = [];
8355
+ const traceContext = options?.traceContext ?? {};
8356
+ const sessionManager = createSandboxSessionManager({
8357
+ sandboxId: options?.sandboxId,
8358
+ sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8359
+ timeoutMs: options?.timeoutMs,
8360
+ traceContext,
8361
+ onStatus: options?.onStatus
8362
+ });
8363
+ const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8364
+ const logSandboxBootRequest = (trigger, details = {}) => {
8365
+ if (sessionManager.getSandboxId()) {
8366
+ return;
8367
+ }
8368
+ logInfo(
8369
+ "sandbox_boot_requested",
8370
+ traceContext,
8371
+ {
8372
+ "app.sandbox.boot.trigger": trigger,
8373
+ ...details
8374
+ },
8375
+ "Sandbox boot requested"
8376
+ );
8377
+ };
8378
+ const executeBashTool = async (rawInput, command) => {
8379
+ const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
8380
+ const env = parseEnv(rawInput.env);
8381
+ logSandboxBootRequest("tool.bash", {
8382
+ "app.sandbox.command_length": command.length
8383
+ });
8384
+ const executeBash = (await sessionManager.ensureToolExecutors()).bash;
8385
+ const result = await withSandboxSpan(
8386
+ "bash",
8387
+ "process.exec",
8388
+ {
8389
+ "process.executable.name": "bash"
8390
+ },
8391
+ async () => {
8392
+ try {
8393
+ const response = await executeBash({
8394
+ command,
8395
+ ...headerTransforms ? { headerTransforms } : {},
8396
+ ...env ? { env } : {}
8397
+ });
8398
+ setSpanAttributes({
8399
+ "process.exit.code": response.exitCode,
8400
+ "app.sandbox.stdout_bytes": Buffer.byteLength(
8401
+ response.stdout ?? "",
8402
+ "utf8"
8403
+ ),
8404
+ "app.sandbox.stderr_bytes": Buffer.byteLength(
8405
+ response.stderr ?? "",
8406
+ "utf8"
8407
+ ),
8408
+ ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
8409
+ });
8410
+ setSpanStatus(response.exitCode === 0 ? "ok" : "error");
8411
+ return response;
8412
+ } catch (error) {
8413
+ setSpanAttributes({
8414
+ "error.type": error instanceof Error ? error.name : "sandbox_execute_error"
8415
+ });
8416
+ setSpanStatus("error");
8417
+ throw error;
8418
+ }
8419
+ }
8420
+ );
8421
+ return {
8422
+ result: {
8423
+ ok: result.exitCode === 0,
8424
+ command,
8425
+ cwd: SANDBOX_WORKSPACE_ROOT,
8426
+ exit_code: result.exitCode,
8427
+ signal: null,
8428
+ timed_out: false,
8429
+ stdout: result.stdout,
8430
+ stderr: result.stderr,
8431
+ stdout_truncated: result.stdoutTruncated,
8432
+ stderr_truncated: result.stderrTruncated
8433
+ }
8434
+ };
8435
+ };
8436
+ const executeReadFileTool = async (rawInput) => {
8437
+ const filePath = String(rawInput.path ?? "").trim();
8438
+ if (!filePath) {
8439
+ throw new Error("path is required");
8440
+ }
8441
+ if (!sessionManager.getSandboxId()) {
8442
+ const hostSkillPath = resolveHostSkillPath(availableSkills, filePath);
8443
+ if (hostSkillPath) {
8444
+ try {
8445
+ const content = await fs4.readFile(hostSkillPath, "utf8");
8446
+ setSpanAttributes({
8447
+ "app.sandbox.path.length": filePath.length,
8448
+ "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8449
+ "app.sandbox.read.chars": content.length,
8450
+ "app.skill.virtual_read": true
8451
+ });
8452
+ setSpanStatus("ok");
8453
+ return {
8454
+ result: {
8455
+ content,
8456
+ path: filePath,
8457
+ success: true
8458
+ }
8459
+ };
8460
+ } catch (error) {
8461
+ if (!isHostFileMissingError(error)) {
8462
+ throw error;
8463
+ }
8464
+ }
8465
+ }
8466
+ }
8467
+ logSandboxBootRequest("tool.readFile", {
8468
+ "file.path": filePath
8469
+ });
8470
+ const executeReadFile = (await sessionManager.ensureToolExecutors()).readFile;
8471
+ const result = await withSandboxSpan(
8472
+ "sandbox.readFile",
8473
+ "sandbox.fs.read",
8474
+ {
8475
+ "app.sandbox.path.length": filePath.length
8476
+ },
8477
+ async () => {
8478
+ const response = await executeReadFile({ path: filePath });
8479
+ const content = String(response.content ?? "");
8480
+ setSpanAttributes({
8481
+ "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8482
+ "app.sandbox.read.chars": content.length
8483
+ });
8484
+ setSpanStatus("ok");
8485
+ return {
8486
+ content,
8487
+ path: filePath,
8488
+ success: true
8489
+ };
8490
+ }
8491
+ );
8492
+ return { result };
8493
+ };
8494
+ const executeWriteFileTool = async (rawInput) => {
8495
+ const filePath = String(rawInput.path ?? "").trim();
8496
+ if (!filePath) {
8497
+ throw new Error("path is required");
8498
+ }
8499
+ const content = String(rawInput.content ?? "");
8500
+ logSandboxBootRequest("tool.writeFile", {
8501
+ "file.path": filePath
8502
+ });
8503
+ const executeWriteFile = (await sessionManager.ensureToolExecutors()).writeFile;
8504
+ await withSandboxSpan(
8505
+ "sandbox.writeFile",
8506
+ "sandbox.fs.write",
8507
+ {
8508
+ "app.sandbox.path.length": filePath.length,
8509
+ "app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
8510
+ },
8511
+ async () => {
8085
8512
  try {
8086
- const commandResult2 = await activeSandbox.runCommand({
8087
- cmd: "bash",
8088
- args: ["-c", script],
8089
- cwd: SANDBOX_WORKSPACE_ROOT
8090
- });
8091
- const maxOutputLength = Number.parseInt(
8092
- process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
8093
- 10
8094
- );
8095
- const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
8096
- const stdoutRaw = await commandResult2.stdout();
8097
- const stderrRaw = await commandResult2.stderr();
8098
- const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
8099
- const stderr = truncateOutput(stderrRaw, boundedOutputLength);
8100
- result = {
8101
- stdout: stdout.value,
8102
- stderr: stderr.value,
8103
- exitCode: commandResult2.exitCode,
8104
- stdoutTruncated: stdout.truncated,
8105
- stderrTruncated: stderr.truncated
8106
- };
8513
+ await executeWriteFile({ path: filePath, content });
8107
8514
  } catch (error) {
8108
- commandError = error;
8109
- throw error;
8110
- } finally {
8111
- if (headerTransforms && headerTransforms.length > 0) {
8112
- try {
8113
- await activeSandbox.updateNetworkPolicy(restoreNetworkPolicy);
8114
- } catch (error) {
8115
- restoreError = error;
8116
- await invalidateSandboxInstance(activeSandbox, error);
8117
- }
8118
- }
8119
- }
8120
- if (restoreError && !commandError) {
8121
- throw restoreError;
8515
+ throwSandboxOperationError("sandbox writeFile", error);
8122
8516
  }
8123
- return result;
8124
- },
8125
- readFile: async (input) => await executeReadFile(input, {
8126
- toolCallId: "sandbox-read-file",
8127
- messages: []
8128
- }),
8129
- writeFile: async (input) => await executeWriteFile(input, {
8130
- toolCallId: "sandbox-write-file",
8131
- messages: []
8132
- })
8517
+ setSpanStatus("ok");
8518
+ }
8519
+ );
8520
+ return {
8521
+ result: {
8522
+ ok: true,
8523
+ path: filePath,
8524
+ bytes_written: Buffer.byteLength(content, "utf8")
8525
+ }
8133
8526
  };
8134
- return toolExecutors;
8135
8527
  };
8136
8528
  const execute = async (params) => {
8137
8529
  const rawInput = params.input ?? {};
@@ -8146,195 +8538,37 @@ function createSandboxExecutor(options) {
8146
8538
  return { result: custom.result };
8147
8539
  }
8148
8540
  }
8149
- }
8150
- const activeSandbox = await acquireSandbox();
8151
- const keepAliveMs = Number.parseInt(
8152
- process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
8153
- 10
8154
- );
8155
- if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
8156
- try {
8157
- await withSandboxSpan(
8158
- "sandbox.keepalive.extend",
8159
- "sandbox.keepalive",
8160
- {
8161
- "app.sandbox.keepalive_ms": keepAliveMs
8162
- },
8163
- async () => {
8164
- await activeSandbox.extendTimeout(keepAliveMs);
8165
- }
8166
- );
8167
- } catch {
8168
- }
8169
- }
8170
- if (params.toolName === "bash") {
8171
- const command = bashCommand;
8172
- const headerTransformsInput = rawInput.headerTransforms;
8173
- const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
8174
- (value) => Boolean(value && typeof value === "object")
8175
- ).map((transform) => ({
8176
- domain: String(transform.domain ?? "").trim(),
8177
- headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
8178
- Object.entries(
8179
- transform.headers
8180
- ).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8181
- ) : {}
8182
- })).filter(
8183
- (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
8184
- ) : void 0;
8185
- const envInput = rawInput.env;
8186
- const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
8187
- Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8188
- ) : void 0;
8189
- const executeBash = (await getToolExecutors()).bash;
8190
- const result = await withSandboxSpan(
8191
- "bash",
8192
- "process.exec",
8193
- {
8194
- "process.executable.name": "bash"
8195
- },
8196
- async () => {
8197
- try {
8198
- const response = await executeBash({
8199
- command,
8200
- ...headerTransforms ? { headerTransforms } : {},
8201
- ...env ? { env } : {}
8202
- });
8203
- setSpanAttributes({
8204
- "process.exit.code": response.exitCode,
8205
- "app.sandbox.stdout_bytes": Buffer.byteLength(
8206
- response.stdout ?? "",
8207
- "utf8"
8208
- ),
8209
- "app.sandbox.stderr_bytes": Buffer.byteLength(
8210
- response.stderr ?? "",
8211
- "utf8"
8212
- ),
8213
- ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
8214
- });
8215
- setSpanStatus(response.exitCode === 0 ? "ok" : "error");
8216
- return response;
8217
- } catch (error) {
8218
- setSpanAttributes({
8219
- "error.type": error instanceof Error ? error.name : "sandbox_execute_error"
8220
- });
8221
- setSpanStatus("error");
8222
- throw error;
8223
- }
8224
- }
8225
- );
8226
- return {
8227
- result: {
8228
- ok: result.exitCode === 0,
8229
- command,
8230
- cwd: SANDBOX_WORKSPACE_ROOT,
8231
- exit_code: result.exitCode,
8232
- signal: null,
8233
- timed_out: false,
8234
- stdout: result.stdout,
8235
- stderr: result.stderr,
8236
- stdout_truncated: result.stdoutTruncated,
8237
- stderr_truncated: result.stderrTruncated
8238
- }
8239
- };
8541
+ return await executeBashTool(rawInput, bashCommand);
8240
8542
  }
8241
8543
  if (params.toolName === "readFile") {
8242
- const filePath = String(rawInput.path ?? "").trim();
8243
- if (!filePath) {
8244
- throw new Error("path is required");
8245
- }
8246
- const executeReadFile = (await getToolExecutors()).readFile;
8247
- const result = await withSandboxSpan(
8248
- "sandbox.readFile",
8249
- "sandbox.fs.read",
8250
- {
8251
- "app.sandbox.path.length": filePath.length
8252
- },
8253
- async () => {
8254
- const response = await executeReadFile({ path: filePath });
8255
- const content = String(response.content ?? "");
8256
- setSpanAttributes({
8257
- "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8258
- "app.sandbox.read.chars": content.length
8259
- });
8260
- setSpanStatus("ok");
8261
- return {
8262
- content,
8263
- path: filePath,
8264
- success: true
8265
- };
8266
- }
8267
- );
8268
- return { result };
8544
+ return await executeReadFileTool(rawInput);
8269
8545
  }
8270
8546
  if (params.toolName === "writeFile") {
8271
- const filePath = String(rawInput.path ?? "").trim();
8272
- if (!filePath) {
8273
- throw new Error("path is required");
8274
- }
8275
- const content = String(rawInput.content ?? "");
8276
- const executeWriteFile = (await getToolExecutors()).writeFile;
8277
- await withSandboxSpan(
8278
- "sandbox.writeFile",
8279
- "sandbox.fs.write",
8280
- {
8281
- "app.sandbox.path.length": filePath.length,
8282
- "app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
8283
- },
8284
- async () => {
8285
- try {
8286
- await executeWriteFile({ path: filePath, content });
8287
- setSpanStatus("ok");
8288
- } catch (error) {
8289
- throwSandboxOperationError("sandbox writeFile", error);
8290
- }
8291
- }
8292
- );
8293
- return {
8294
- result: {
8295
- ok: true,
8296
- path: filePath,
8297
- bytes_written: Buffer.byteLength(content, "utf8")
8298
- }
8299
- };
8547
+ return await executeWriteFileTool(rawInput);
8300
8548
  }
8301
8549
  throw new Error(`unsupported sandbox tool: ${params.toolName}`);
8302
8550
  };
8303
- const dispose = async () => {
8304
- if (!sandbox) {
8305
- return;
8306
- }
8307
- await withSandboxSpan(
8308
- "sandbox.stop",
8309
- "sandbox.stop",
8310
- {
8311
- "app.sandbox.stop.blocking": true
8312
- },
8313
- async () => {
8314
- await sandbox.stop({ blocking: true });
8315
- }
8316
- );
8317
- sandbox = null;
8318
- toolExecutors = void 0;
8319
- };
8320
8551
  return {
8321
8552
  configureSkills(skills) {
8322
8553
  availableSkills = [...skills];
8554
+ sessionManager.configureSkills(skills);
8323
8555
  },
8324
8556
  getSandboxId() {
8325
- return sandbox?.sandboxId ?? sandboxIdHint;
8557
+ return sessionManager.getSandboxId();
8326
8558
  },
8327
8559
  getDependencyProfileHash() {
8328
- return dependencyProfileHash;
8560
+ return sessionManager.getDependencyProfileHash();
8329
8561
  },
8330
8562
  canExecute(toolName) {
8331
8563
  return SANDBOX_TOOL_NAMES.has(toolName);
8332
8564
  },
8333
8565
  async createSandbox() {
8334
- return await acquireSandbox();
8566
+ return createSandboxWorkspace(await sessionManager.createSandbox());
8335
8567
  },
8336
8568
  execute,
8337
- dispose
8569
+ async dispose() {
8570
+ await sessionManager.dispose();
8571
+ }
8338
8572
  };
8339
8573
  }
8340
8574
 
@@ -9156,6 +9390,14 @@ async function generateAssistantReply(messageText, context = {}) {
9156
9390
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
9157
9391
  let loadedSkillNamesForResume = [];
9158
9392
  let mcpToolManager;
9393
+ let sandboxExecutor;
9394
+ const getSandboxMetadata = () => sandboxExecutor ? {
9395
+ sandboxId: sandboxExecutor.getSandboxId(),
9396
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
9397
+ } : {
9398
+ sandboxId: lastKnownSandboxId,
9399
+ sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash
9400
+ };
9159
9401
  try {
9160
9402
  const shouldTrace = shouldEmitDevAgentTrace();
9161
9403
  const spanContext = {
@@ -9228,7 +9470,7 @@ async function generateAssistantReply(messageText, context = {}) {
9228
9470
  resolveConfiguration: async (key) => configurationValues[key]
9229
9471
  });
9230
9472
  const providerAuthActions = /* @__PURE__ */ new Map();
9231
- const sandboxExecutor = createSandboxExecutor({
9473
+ sandboxExecutor = createSandboxExecutor({
9232
9474
  sandboxId: context.sandbox?.sandboxId,
9233
9475
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
9234
9476
  traceContext: spanContext,
@@ -9255,10 +9497,52 @@ async function generateAssistantReply(messageText, context = {}) {
9255
9497
  return result.handled ? { handled: true, result: result.result } : { handled: false };
9256
9498
  }
9257
9499
  });
9258
- lastKnownSandboxId = sandboxExecutor.getSandboxId();
9259
- lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
9500
+ const currentSandboxExecutor = sandboxExecutor;
9260
9501
  sandboxExecutor.configureSkills(availableSkills);
9261
- const sandbox = await sandboxExecutor.createSandbox();
9502
+ let sandboxPromise;
9503
+ let sandboxPromiseId;
9504
+ const clearSandboxPromise = () => {
9505
+ sandboxPromise = void 0;
9506
+ sandboxPromiseId = void 0;
9507
+ };
9508
+ const getSandbox = (reason) => {
9509
+ const currentSandboxId = currentSandboxExecutor.getSandboxId();
9510
+ if (sandboxPromise && sandboxPromiseId && currentSandboxId !== sandboxPromiseId) {
9511
+ clearSandboxPromise();
9512
+ }
9513
+ if (!sandboxPromise) {
9514
+ logInfo(
9515
+ "sandbox_boot_requested",
9516
+ spanContext,
9517
+ {
9518
+ "app.sandbox.boot.trigger": reason.trigger,
9519
+ ...reason.path ? { "file.path": reason.path } : {},
9520
+ ...reason.cmd ? { "process.executable.name": reason.cmd } : {},
9521
+ ...reason.cwd ? { "file.directory": reason.cwd } : {}
9522
+ },
9523
+ "Lazy sandbox boot requested"
9524
+ );
9525
+ sandboxPromise = currentSandboxExecutor.createSandbox().then((sandbox2) => {
9526
+ sandboxPromiseId = sandbox2.sandboxId;
9527
+ return sandbox2;
9528
+ }).catch((error) => {
9529
+ clearSandboxPromise();
9530
+ throw error;
9531
+ });
9532
+ }
9533
+ return sandboxPromise;
9534
+ };
9535
+ const sandbox = {
9536
+ readFileToBuffer: async (input) => (await getSandbox({
9537
+ trigger: "workspace.readFileToBuffer",
9538
+ path: input.path
9539
+ })).readFileToBuffer(input),
9540
+ runCommand: async (input) => (await getSandbox({
9541
+ trigger: "workspace.runCommand",
9542
+ cmd: input.cmd,
9543
+ cwd: input.cwd
9544
+ })).runCommand(input)
9545
+ };
9262
9546
  for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
9263
9547
  const preloaded = await skillSandbox.loadSkill(skillName);
9264
9548
  if (preloaded) {
@@ -9397,16 +9681,32 @@ async function generateAssistantReply(messageText, context = {}) {
9397
9681
  });
9398
9682
  const userContentParts = [{ type: "text", text: userTurnText }];
9399
9683
  for (const attachment of context.userAttachments ?? []) {
9400
- if (attachment.mediaType.startsWith("image/")) {
9684
+ if (attachment.promptText) {
9685
+ userContentParts.push({
9686
+ type: "text",
9687
+ text: attachment.promptText
9688
+ });
9689
+ } else if (attachment.mediaType.startsWith("image/")) {
9690
+ if (!attachment.data) {
9691
+ throw new Error("Image attachment is missing image data");
9692
+ }
9401
9693
  userContentParts.push({
9402
9694
  type: "image",
9403
9695
  data: attachment.data.toString("base64"),
9404
9696
  mimeType: attachment.mediaType
9405
9697
  });
9406
9698
  } else {
9699
+ if (!attachment.data) {
9700
+ throw new Error("Attachment is missing attachment data");
9701
+ }
9702
+ const promptAttachment = {
9703
+ data: attachment.data,
9704
+ mediaType: attachment.mediaType,
9705
+ filename: attachment.filename
9706
+ };
9407
9707
  userContentParts.push({
9408
9708
  type: "text",
9409
- text: encodeNonImageAttachmentForPrompt(attachment)
9709
+ text: encodeNonImageAttachmentForPrompt(promptAttachment)
9410
9710
  });
9411
9711
  }
9412
9712
  }
@@ -9603,8 +9903,8 @@ async function generateAssistantReply(messageText, context = {}) {
9603
9903
  replyFiles,
9604
9904
  artifactStatePatch,
9605
9905
  toolCalls,
9606
- sandboxId: sandboxExecutor.getSandboxId(),
9607
- sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
9906
+ sandboxId: currentSandboxExecutor.getSandboxId(),
9907
+ sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
9608
9908
  generatedFileCount: generatedFiles.length,
9609
9909
  hasTextDeltaCallback: Boolean(context.onTextDelta),
9610
9910
  shouldTrace,
@@ -9655,8 +9955,7 @@ async function generateAssistantReply(messageText, context = {}) {
9655
9955
  const message = error instanceof Error ? error.message : String(error);
9656
9956
  return {
9657
9957
  text: `Error: ${message}`,
9658
- sandboxId: lastKnownSandboxId,
9659
- sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
9958
+ ...getSandboxMetadata(),
9660
9959
  diagnostics: {
9661
9960
  outcome: "provider_error",
9662
9961
  modelId: botConfig.modelId,
@@ -10260,7 +10559,7 @@ async function GET4(request, provider, waitUntil) {
10260
10559
  }
10261
10560
 
10262
10561
  // src/chat/slack/app-home.ts
10263
- import fs4 from "fs";
10562
+ import fs5 from "fs";
10264
10563
  import path5 from "path";
10265
10564
  var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
10266
10565
  var MAX_HOME_SKILLS = 6;
@@ -10275,7 +10574,7 @@ function clampSectionText(text) {
10275
10574
  function loadAboutText() {
10276
10575
  const aboutPath = path5.join(homeDir(), "ABOUT.md");
10277
10576
  try {
10278
- const raw = fs4.readFileSync(aboutPath, "utf8").trim();
10577
+ const raw = fs5.readFileSync(aboutPath, "utf8").trim();
10279
10578
  if (raw.length > 0) {
10280
10579
  return clampSectionText(raw);
10281
10580
  }
@@ -10625,7 +10924,7 @@ var replyDecisionSchema = z.object({
10625
10924
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
10626
10925
  var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
10627
10926
  var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
10628
- var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]+:\s+([\s\S]+)$/i;
10927
+ var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
10629
10928
  var THREAD_OPTOUT_PATTERNS = [
10630
10929
  /\bstop (?:watching|replying|participating)\b/i,
10631
10930
  /\bstay out\b/i,
@@ -10633,6 +10932,11 @@ var THREAD_OPTOUT_PATTERNS = [
10633
10932
  /\bunsubscribe\b/i,
10634
10933
  /\bleave (?:this )?thread\b/i
10635
10934
  ];
10935
+ var ACKNOWLEDGMENT_ONLY_RE = /^(?:thanks(?: you)?|thank you|thx|ty|got it|sounds good|sgtm|lgtm|ok(?:ay)?|cool|nice|perfect|awesome|great|makes sense|understood|roger|yep|yup|kk|on it|will do)(?:[.!]+)?$/i;
10936
+ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|your last answer|what did you just say|what do you mean|what did you mean|explain(?: that| this| it| more)?|clarify(?: that| this| it)?|expand(?: on)?(?: that| this| it)?|elaborate(?: on)?(?: that| this| it)?|say more)\b/i;
10937
+ var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
10938
+ var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
10939
+ var RECENT_THREAD_WINDOW = 6;
10636
10940
  function escapeRegExp(value) {
10637
10941
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10638
10942
  }
@@ -10669,36 +10973,113 @@ function isThreadOptOutInstruction(rawText, text) {
10669
10973
  (pattern) => pattern.test(rawText) || pattern.test(text)
10670
10974
  );
10671
10975
  }
10672
- function getTranscriptMessageHints(conversationContext) {
10976
+ function isAcknowledgmentOnly(text) {
10977
+ return ACKNOWLEDGMENT_ONLY_RE.test(text.trim());
10978
+ }
10979
+ function hasDirectedFollowUpCue(text) {
10980
+ return DIRECTED_FOLLOW_UP_CUE_RE.test(text.trim());
10981
+ }
10982
+ function isTerseClarification(text) {
10983
+ return TERSE_CLARIFICATION_RE.test(text.trim());
10984
+ }
10985
+ function isGenericImmediateSideConversation(text) {
10986
+ const trimmed = text.trim();
10987
+ if (GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE.test(trimmed)) {
10988
+ return true;
10989
+ }
10990
+ if (!trimmed.toLowerCase().startsWith("what about")) {
10991
+ return false;
10992
+ }
10993
+ const wordCount = trimmed.split(/\s+/).map((part) => part.trim()).filter(Boolean).length;
10994
+ return wordCount > 3;
10995
+ }
10996
+ function parseTranscriptMessages(conversationContext) {
10673
10997
  if (!conversationContext) {
10674
- return {
10675
- latestPriorMessageRole: "[none]",
10676
- latestPriorAssistantMessage: "[none]"
10677
- };
10998
+ return [];
10678
10999
  }
11000
+ const messages = [];
10679
11001
  const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
10680
- let latestPriorMessageRole = "[none]";
10681
- let latestPriorAssistantMessage = "[none]";
10682
- for (let index = lines.length - 1; index >= 0; index -= 1) {
10683
- const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
11002
+ for (const line of lines) {
11003
+ const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
10684
11004
  if (!match) {
10685
11005
  continue;
10686
11006
  }
10687
- if (latestPriorMessageRole === "[none]") {
10688
- latestPriorMessageRole = match[1].toLowerCase();
10689
- }
10690
- if (latestPriorAssistantMessage === "[none]" && match[1].toLowerCase() === "assistant") {
10691
- latestPriorAssistantMessage = match[2];
11007
+ messages.push({
11008
+ role: match[1].toLowerCase(),
11009
+ author: match[2]?.trim() || "unknown",
11010
+ text: match[3]?.trim() || ""
11011
+ });
11012
+ }
11013
+ return messages;
11014
+ }
11015
+ function buildRouterSignals(input) {
11016
+ const transcriptMessages = parseTranscriptMessages(input.conversationContext);
11017
+ const recentMessages = transcriptMessages.filter((message) => message.role !== "system").slice(-RECENT_THREAD_WINDOW);
11018
+ const latestPriorMessage = [...transcriptMessages].reverse().find((message) => message.role !== "system");
11019
+ const latestPriorAssistantMessage = [...transcriptMessages].reverse().find((message) => message.role === "assistant");
11020
+ let humanMessagesSinceLastAssistant;
11021
+ let humanMessageCount = 0;
11022
+ for (let index = transcriptMessages.length - 1; index >= 0; index -= 1) {
11023
+ const message = transcriptMessages[index];
11024
+ if (!message || message.role === "system") {
11025
+ continue;
10692
11026
  }
10693
- if (latestPriorMessageRole !== "[none]" && latestPriorAssistantMessage !== "[none]") {
11027
+ if (message.role === "assistant") {
11028
+ humanMessagesSinceLastAssistant = humanMessageCount;
10694
11029
  break;
10695
11030
  }
11031
+ humanMessageCount += 1;
10696
11032
  }
10697
11033
  return {
10698
- latestPriorMessageRole,
10699
- latestPriorAssistantMessage
11034
+ assistantWasLastSpeaker: latestPriorMessage?.role === "assistant",
11035
+ currentMessageHasDirectedFollowUpCue: hasDirectedFollowUpCue(input.text),
11036
+ currentMessageHasAttachments: Boolean(input.hasAttachments),
11037
+ currentMessageIsTerseClarification: isTerseClarification(input.text),
11038
+ humanMessagesSinceLastAssistant,
11039
+ latestPriorAssistantMessage: latestPriorAssistantMessage?.text || "[none]",
11040
+ latestPriorMessageRole: latestPriorMessage?.role || "[none]",
11041
+ recentMessages
10700
11042
  };
10701
11043
  }
11044
+ function buildRouterPrompt(rawText, signals) {
11045
+ const recentThread = signals.recentMessages.length > 0 ? signals.recentMessages.map(
11046
+ (message) => escapeXml(`[${message.role}] ${message.author}: ${message.text}`)
11047
+ ).join("\n") : "[none]";
11048
+ return [
11049
+ `<latest-message>${escapeXml(rawText.trim() || "[attachment-only message]")}</latest-message>`,
11050
+ "<routing-signals>",
11051
+ `assistant_was_last_speaker=${signals.assistantWasLastSpeaker ? "true" : "false"}`,
11052
+ `human_messages_since_last_assistant=${signals.humanMessagesSinceLastAssistant ?? "none"}`,
11053
+ `latest_prior_message_role=${escapeXml(signals.latestPriorMessageRole)}`,
11054
+ `current_message_has_directed_follow_up_cue=${signals.currentMessageHasDirectedFollowUpCue ? "true" : "false"}`,
11055
+ `current_message_is_terse_clarification=${signals.currentMessageIsTerseClarification ? "true" : "false"}`,
11056
+ `current_message_has_attachments=${signals.currentMessageHasAttachments ? "true" : "false"}`,
11057
+ "</routing-signals>",
11058
+ `<latest-prior-assistant-message>${escapeXml(
11059
+ signals.latestPriorAssistantMessage
11060
+ )}</latest-prior-assistant-message>`,
11061
+ "<recent-thread>",
11062
+ recentThread,
11063
+ "</recent-thread>"
11064
+ ].join("\n");
11065
+ }
11066
+ function getReplyConfidenceThreshold(signals) {
11067
+ let threshold = ROUTER_CONFIDENCE_THRESHOLD;
11068
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0) {
11069
+ if (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification) {
11070
+ threshold = 0.65;
11071
+ } else {
11072
+ threshold = 0.9;
11073
+ }
11074
+ } else if (signals.humanMessagesSinceLastAssistant === 1) {
11075
+ threshold = signals.currentMessageHasDirectedFollowUpCue ? 0.8 : 0.9;
11076
+ } else if (signals.humanMessagesSinceLastAssistant === void 0) {
11077
+ threshold = 0.85;
11078
+ } else if (signals.humanMessagesSinceLastAssistant >= 2) {
11079
+ threshold = 0.9;
11080
+ }
11081
+ return Math.max(0.6, Math.min(0.9, threshold));
11082
+ }
10702
11083
  function getSubscribedReplyPreflightDecision(args) {
10703
11084
  const text = args.text.trim();
10704
11085
  const rawText = args.rawText.trim();
@@ -10719,54 +11100,27 @@ function getSubscribedReplyPreflightDecision(args) {
10719
11100
  reasonDetail: leadingOtherPartyAddress
10720
11101
  };
10721
11102
  }
10722
- function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
10723
- const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
11103
+ function buildRouterSystemPrompt(botUserName) {
10724
11104
  return [
10725
11105
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
10726
11106
  "Decide whether Junior should reply to the latest message.",
10727
11107
  "Subscribed threads are passive by default.",
10728
- "Default to should_reply=false unless the user is clearly asking Junior for help or follow-up.",
10729
- "A direct @mention is a strong signal to reply unless the message is clearly telling Junior to stop participating.",
10730
- "",
10731
- "Reply should be true only when the user is clearly asking Junior a question, requesting help,",
10732
- "or when a direct follow-up is contextually aimed at Junior's previous response in the thread context.",
10733
- "",
10734
- "Reply should be false for side conversations between humans, acknowledgements,",
10735
- "status chatter, or messages not seeking assistant input.",
10736
- "Junior must not participate in casual banter or keep chiming in just because it replied earlier.",
10737
- "",
10738
- "Examples of messages Junior should NOT reply to (should_reply=false):",
10739
- "- Questions between humans: 'Is that the right approach?', 'Can you check on this?', 'Did you deploy that?'",
10740
- "- Acknowledgments: 'thanks', '+1', 'lgtm', 'ok cool', 'sounds good', 'nice'",
10741
- "- Status updates: 'I just pushed a fix', 'Deploying now', 'Build is green'",
10742
- "- General thread discussion: 'What about the billing issue?', 'I think we should revert'",
10743
- "- Reactions to work: 'That looks wrong', 'Nice catch', 'Hmm interesting'",
10744
- "",
10745
- "Examples of messages Junior SHOULD reply to (should_reply=true):",
10746
- "- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
10747
- "- Self-referential follow-ups after Junior just answered: 'What did you just say about the budget?', 'Can you explain your last response in more detail?'",
10748
- "- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
10749
- "",
10750
- "Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
10751
- "using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
10752
- "Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
10753
- "still should_reply=false unless it clearly asks Junior for help.",
10754
- "",
10755
- "When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
10756
- "",
10757
- "If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
10758
- "set should_unsubscribe=true and should_reply=false.",
10759
- "Use should_unsubscribe only for clear thread opt-out instructions, not for ordinary side conversation.",
10760
- "If uncertain, set should_reply=false and use low confidence.",
11108
+ "Reply true only when the latest message is aimed at Junior.",
11109
+ "Use who currently has the conversation floor, not just topic overlap.",
11110
+ "If Junior was the last speaker, only a clear turn back to Junior should count as an implicit follow-up.",
11111
+ "Terse clarifications like 'which one?' or 'why?' right after Junior answers can be should_reply=true.",
11112
+ "Direct self-reference to Junior's prior answer like 'what did you just say?' or 'explain that more' can be should_reply=true.",
11113
+ "If one or more humans spoke after Junior, require a clear turn back to Junior. Shared domain vocabulary alone is not enough.",
11114
+ "Questions like 'what about auth?' or 'can you check on this?' are usually human-to-human unless the thread clearly turns back to Junior.",
11115
+ "A vague question like 'is that the right approach?' is still should_reply=false unless it clearly turns back to Junior.",
11116
+ "Acknowledgments, reactions, status chatter, and team coordination should be should_reply=false.",
11117
+ "If the latest message clearly tells Junior to stop watching, replying, or participating, set should_unsubscribe=true and should_reply=false.",
11118
+ "When uncertain, prefer should_reply=false with low confidence.",
10761
11119
  "",
10762
11120
  "Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
10763
11121
  "Do not return any extra keys.",
10764
11122
  "",
10765
- `<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
10766
- `<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
10767
- `<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
10768
- `<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
10769
- `<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
11123
+ `<assistant-name>${escapeXml(botUserName)}</assistant-name>`
10770
11124
  ].join("\n");
10771
11125
  }
10772
11126
  async function decideSubscribedThreadReply(args) {
@@ -10781,11 +11135,16 @@ async function decideSubscribedThreadReply(args) {
10781
11135
  if (preflightDecision) {
10782
11136
  return preflightDecision;
10783
11137
  }
11138
+ const signals = buildRouterSignals(args.input);
10784
11139
  if (!text && !args.input.hasAttachments) {
10785
11140
  return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
10786
11141
  }
10787
- if (!text && args.input.hasAttachments) {
10788
- return { shouldReply: true, reason: "attachment_only" /* AttachmentOnly */ };
11142
+ if (!args.input.isExplicitMention && !args.input.hasAttachments && isAcknowledgmentOnly(text)) {
11143
+ return {
11144
+ shouldReply: false,
11145
+ reason: "side_conversation" /* SideConversation */,
11146
+ reasonDetail: "acknowledgment"
11147
+ };
10789
11148
  }
10790
11149
  if (args.input.isExplicitMention) {
10791
11150
  if (isThreadOptOutInstruction(rawText, text)) {
@@ -10801,18 +11160,21 @@ async function decideSubscribedThreadReply(args) {
10801
11160
  reason: "explicit_mention" /* ExplicitMention */
10802
11161
  };
10803
11162
  }
11163
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
11164
+ return {
11165
+ shouldReply: false,
11166
+ reason: "side_conversation" /* SideConversation */,
11167
+ reasonDetail: "generic immediate side conversation"
11168
+ };
11169
+ }
10804
11170
  try {
10805
11171
  const result = await args.completeObject({
10806
11172
  modelId: args.modelId,
10807
11173
  schema: replyDecisionSchema,
10808
11174
  maxTokens: 120,
10809
11175
  temperature: 0,
10810
- system: buildRouterSystemPrompt(
10811
- args.botUserName,
10812
- args.input.conversationContext,
10813
- args.input.isExplicitMention
10814
- ),
10815
- prompt: rawText,
11176
+ system: buildRouterSystemPrompt(args.botUserName),
11177
+ prompt: buildRouterPrompt(rawText, signals),
10816
11178
  metadata: {
10817
11179
  modelId: args.modelId,
10818
11180
  threadId: args.input.context.threadId ?? "",
@@ -10823,6 +11185,7 @@ async function decideSubscribedThreadReply(args) {
10823
11185
  });
10824
11186
  const parsed = replyDecisionSchema.parse(result.object);
10825
11187
  const reason = parsed.reason?.trim() || "classifier";
11188
+ const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
10826
11189
  if (parsed.should_unsubscribe) {
10827
11190
  if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
10828
11191
  return {
@@ -10845,7 +11208,7 @@ async function decideSubscribedThreadReply(args) {
10845
11208
  reasonDetail: reason
10846
11209
  };
10847
11210
  }
10848
- if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
11211
+ if (parsed.confidence < replyConfidenceThreshold) {
10849
11212
  return {
10850
11213
  shouldReply: false,
10851
11214
  reason: "low_confidence" /* LowConfidence */,
@@ -11440,16 +11803,166 @@ var MAX_USER_ATTACHMENTS = 3;
11440
11803
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
11441
11804
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
11442
11805
  var MAX_VISION_SUMMARY_CHARS = 500;
11443
- async function resolveUserAttachments(attachments, context) {
11806
+ function isVisionEnabled() {
11807
+ return Boolean(botConfig.visionModelId);
11808
+ }
11809
+ var ImageAttachmentProcessingError = class extends Error {
11810
+ constructor(message) {
11811
+ super(message);
11812
+ this.name = "ImageAttachmentProcessingError";
11813
+ }
11814
+ };
11815
+ function buildImageAttachmentPromptText(args) {
11816
+ return [
11817
+ "<image-attachment>",
11818
+ `filename: ${args.filename ?? "unnamed"}`,
11819
+ `media_type: ${args.mediaType}`,
11820
+ "<summary>",
11821
+ args.summary,
11822
+ "</summary>",
11823
+ "</image-attachment>"
11824
+ ].join("\n");
11825
+ }
11826
+ async function summarizeImageWithVision(args) {
11827
+ const visionModelId = botConfig.visionModelId;
11828
+ if (!visionModelId) {
11829
+ return void 0;
11830
+ }
11831
+ const result = await args.completeText({
11832
+ modelId: visionModelId,
11833
+ temperature: 0,
11834
+ maxTokens: args.maxTokens,
11835
+ messages: [
11836
+ {
11837
+ role: "user",
11838
+ content: [
11839
+ {
11840
+ type: "text",
11841
+ text: args.prompt
11842
+ },
11843
+ {
11844
+ type: "image",
11845
+ data: args.imageData.toString("base64"),
11846
+ mimeType: args.mimeType
11847
+ }
11848
+ ],
11849
+ timestamp: Date.now()
11850
+ }
11851
+ ],
11852
+ metadata: {
11853
+ modelId: visionModelId,
11854
+ ...args.metadata
11855
+ }
11856
+ });
11857
+ const summary = result.text.trim().replace(/\s+/g, " ");
11858
+ return summary || void 0;
11859
+ }
11860
+ function truncateVisionSummary(summary) {
11861
+ return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
11862
+ }
11863
+ function getCachedImageSummaries(args) {
11864
+ if (!args.conversation || !args.messageTs) {
11865
+ return [];
11866
+ }
11867
+ const conversationMessage = args.conversation.messages.find(
11868
+ (message) => getConversationMessageSlackTs(message) === args.messageTs
11869
+ );
11870
+ if (!conversationMessage) {
11871
+ return [];
11872
+ }
11873
+ return (conversationMessage.meta?.imageFileIds ?? []).map(
11874
+ (fileId) => args.conversation?.vision.byFileId[fileId]?.summary?.trim()
11875
+ );
11876
+ }
11877
+ function createImageAttachmentProcessingError(attachment) {
11878
+ const label = attachment.filename ? `"${attachment.filename}"` : "this image";
11879
+ return new ImageAttachmentProcessingError(
11880
+ `Image attachment ${label} could not be analyzed`
11881
+ );
11882
+ }
11883
+ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
11444
11884
  if (!attachments || attachments.length === 0) {
11445
11885
  return [];
11446
11886
  }
11447
11887
  const results = [];
11888
+ const cachedImageSummaries = getCachedImageSummaries({
11889
+ conversation: context.conversation,
11890
+ messageTs: context.messageTs
11891
+ });
11892
+ let nextCachedImageSummaryIndex = 0;
11448
11893
  for (const attachment of attachments) {
11449
11894
  if (results.length >= MAX_USER_ATTACHMENTS) break;
11450
11895
  if (attachment.type !== "image" && attachment.type !== "file") continue;
11451
11896
  const mediaType = attachment.mimeType ?? "application/octet-stream";
11897
+ const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
11898
+ if (isImageAttachment && !isVisionEnabled()) {
11899
+ continue;
11900
+ }
11452
11901
  try {
11902
+ const resolvedAttachment = {
11903
+ mediaType,
11904
+ filename: attachment.name
11905
+ };
11906
+ if (isImageAttachment) {
11907
+ const cachedSummary = cachedImageSummaries[nextCachedImageSummaryIndex];
11908
+ nextCachedImageSummaryIndex += 1;
11909
+ if (cachedSummary) {
11910
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
11911
+ filename: attachment.name,
11912
+ mediaType,
11913
+ summary: cachedSummary
11914
+ });
11915
+ results.push(resolvedAttachment);
11916
+ continue;
11917
+ }
11918
+ let imageData = null;
11919
+ if (attachment.fetchData) {
11920
+ imageData = await attachment.fetchData();
11921
+ } else if (attachment.data instanceof Buffer) {
11922
+ imageData = attachment.data;
11923
+ }
11924
+ if (!imageData) {
11925
+ throw createImageAttachmentProcessingError({
11926
+ filename: attachment.name
11927
+ });
11928
+ }
11929
+ if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
11930
+ throw createImageAttachmentProcessingError({
11931
+ filename: attachment.name
11932
+ });
11933
+ }
11934
+ const summary = await summarizeImageWithVision({
11935
+ completeText: deps.completeText,
11936
+ imageData,
11937
+ mimeType: mediaType,
11938
+ maxTokens: 220,
11939
+ prompt: [
11940
+ "Extract concise, factual context from this user-provided image.",
11941
+ "Focus on visible text, UI state, charts, diagrams, errors, names, and other concrete details useful for answering the user's current request.",
11942
+ "Do not speculate.",
11943
+ "Return plain text only."
11944
+ ].join(" "),
11945
+ metadata: {
11946
+ threadId: context.threadId ?? "",
11947
+ channelId: context.channelId ?? "",
11948
+ requesterId: context.requesterId ?? "",
11949
+ runId: context.runId ?? "",
11950
+ filename: attachment.name ?? ""
11951
+ }
11952
+ });
11953
+ if (!summary) {
11954
+ throw createImageAttachmentProcessingError({
11955
+ filename: attachment.name
11956
+ });
11957
+ }
11958
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
11959
+ filename: attachment.name,
11960
+ mediaType,
11961
+ summary: truncateVisionSummary(summary)
11962
+ });
11963
+ results.push(resolvedAttachment);
11964
+ continue;
11965
+ }
11453
11966
  let data = null;
11454
11967
  if (attachment.fetchData) {
11455
11968
  data = await attachment.fetchData();
@@ -11476,12 +11989,32 @@ async function resolveUserAttachments(attachments, context) {
11476
11989
  );
11477
11990
  continue;
11478
11991
  }
11479
- results.push({
11480
- data,
11481
- mediaType,
11482
- filename: attachment.name
11483
- });
11992
+ resolvedAttachment.data = data;
11993
+ results.push(resolvedAttachment);
11484
11994
  } catch (error) {
11995
+ if (isImageAttachment) {
11996
+ const attachmentError = error instanceof ImageAttachmentProcessingError ? error : createImageAttachmentProcessingError({
11997
+ filename: attachment.name
11998
+ });
11999
+ logWarn(
12000
+ "image_attachment_processing_failed",
12001
+ {
12002
+ slackThreadId: context.threadId,
12003
+ slackUserId: context.requesterId,
12004
+ slackChannelId: context.channelId,
12005
+ runId: context.runId,
12006
+ assistantUserName: botConfig.userName,
12007
+ modelId: botConfig.visionModelId ?? botConfig.modelId
12008
+ },
12009
+ {
12010
+ "error.message": error instanceof Error ? error.message : String(error),
12011
+ "file.mime_type": mediaType,
12012
+ ...attachment.name ? { "file.name": attachment.name } : {}
12013
+ },
12014
+ "Image attachment processing failed"
12015
+ );
12016
+ throw attachmentError;
12017
+ }
11485
12018
  logWarn(
11486
12019
  "attachment_resolution_failed",
11487
12020
  {
@@ -11503,35 +12036,23 @@ async function resolveUserAttachments(attachments, context) {
11503
12036
  return results;
11504
12037
  }
11505
12038
  async function summarizeConversationImage(args, deps) {
12039
+ const visionModelId = botConfig.visionModelId;
12040
+ if (!visionModelId) {
12041
+ return void 0;
12042
+ }
11506
12043
  try {
11507
- const result = await deps.completeText({
11508
- modelId: botConfig.modelId,
11509
- temperature: 0,
12044
+ const summary = await summarizeImageWithVision({
12045
+ completeText: deps.completeText,
12046
+ imageData: args.imageData,
12047
+ mimeType: args.mimeType,
11510
12048
  maxTokens: 220,
11511
- messages: [
11512
- {
11513
- role: "user",
11514
- content: [
11515
- {
11516
- type: "text",
11517
- text: [
11518
- "Extract concise, factual context from this image for future thread turns.",
11519
- "Focus on visible text, names, titles, companies, and candidate-identifying details.",
11520
- "Do not speculate.",
11521
- "Return plain text only."
11522
- ].join(" ")
11523
- },
11524
- {
11525
- type: "image",
11526
- data: args.imageData.toString("base64"),
11527
- mimeType: args.mimeType
11528
- }
11529
- ],
11530
- timestamp: Date.now()
11531
- }
11532
- ],
12049
+ prompt: [
12050
+ "Extract concise, factual context from this image for future thread turns.",
12051
+ "Focus on visible text, names, titles, companies, and candidate-identifying details.",
12052
+ "Do not speculate.",
12053
+ "Return plain text only."
12054
+ ].join(" "),
11533
12055
  metadata: {
11534
- modelId: botConfig.modelId,
11535
12056
  threadId: args.context.threadId ?? "",
11536
12057
  channelId: args.context.channelId ?? "",
11537
12058
  requesterId: args.context.requesterId ?? "",
@@ -11539,11 +12060,10 @@ async function summarizeConversationImage(args, deps) {
11539
12060
  fileId: args.fileId
11540
12061
  }
11541
12062
  });
11542
- const summary = result.text.trim().replace(/\s+/g, " ");
11543
12063
  if (!summary) {
11544
12064
  return void 0;
11545
12065
  }
11546
- return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
12066
+ return truncateVisionSummary(summary);
11547
12067
  } catch (error) {
11548
12068
  logWarn(
11549
12069
  "conversation_image_vision_failed",
@@ -11553,7 +12073,7 @@ async function summarizeConversationImage(args, deps) {
11553
12073
  slackChannelId: args.context.channelId,
11554
12074
  runId: args.context.runId,
11555
12075
  assistantUserName: botConfig.userName,
11556
- modelId: botConfig.modelId
12076
+ modelId: visionModelId
11557
12077
  },
11558
12078
  {
11559
12079
  "error.message": error instanceof Error ? error.message : String(error),
@@ -11566,6 +12086,9 @@ async function summarizeConversationImage(args, deps) {
11566
12086
  }
11567
12087
  }
11568
12088
  async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
12089
+ if (!isVisionEnabled()) {
12090
+ return;
12091
+ }
11569
12092
  if (!context.channelId || !context.threadTs) {
11570
12093
  return;
11571
12094
  }
@@ -11766,6 +12289,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
11766
12289
  }
11767
12290
  function createVisionContextService(deps) {
11768
12291
  return {
12292
+ resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
11769
12293
  hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
11770
12294
  conversation,
11771
12295
  context,
@@ -11945,13 +12469,15 @@ function createReplyToThread(deps) {
11945
12469
  if (resolvedUserName) {
11946
12470
  setTags({ slackUserName: resolvedUserName });
11947
12471
  }
11948
- const userAttachments = await resolveUserAttachments(
12472
+ const userAttachments = await deps.resolveUserAttachments(
11949
12473
  message.attachments,
11950
12474
  {
11951
12475
  threadId,
11952
12476
  requesterId: message.author.userId,
11953
12477
  channelId,
11954
- runId
12478
+ runId,
12479
+ conversation: preparedState.conversation,
12480
+ messageTs: message.id
11955
12481
  }
11956
12482
  );
11957
12483
  const progress = createProgressReporter({
@@ -12361,7 +12887,7 @@ function createPrepareTurnState(deps) {
12361
12887
  conversation,
12362
12888
  incomingUserMessage
12363
12889
  );
12364
- if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
12890
+ if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
12365
12891
  await deps.hydrateConversationVisionContext(conversation, {
12366
12892
  threadId: args.context.threadId,
12367
12893
  channelId: args.context.channelId,
@@ -12408,6 +12934,7 @@ function createSlackRuntime(options) {
12408
12934
  const replyToThread = createReplyToThread({
12409
12935
  getSlackAdapter: options.getSlackAdapter,
12410
12936
  prepareTurnState,
12937
+ resolveUserAttachments: services.visionContext.resolveUserAttachments,
12411
12938
  services: services.replyExecutor
12412
12939
  });
12413
12940
  return createSlackTurnRuntime({