@sentry/junior 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -5,10 +5,10 @@ import {
5
5
  listCapabilityProviders,
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
- parseSkillInvocation,
9
- stripFrontmatter
10
- } from "./chunk-4XWTSMRF.js";
8
+ parseSkillInvocation
9
+ } from "./chunk-NRSP2MLC.js";
11
10
  import {
11
+ SANDBOX_DATA_ROOT,
12
12
  SANDBOX_SKILLS_ROOT,
13
13
  SANDBOX_WORKSPACE_ROOT,
14
14
  botConfig,
@@ -25,8 +25,9 @@ import {
25
25
  resolveRuntimeDependencySnapshot,
26
26
  runNonInteractiveCommand,
27
27
  sandboxSkillDir,
28
+ sandboxSkillFile,
28
29
  toOptionalTrimmed
29
- } from "./chunk-XYOKYK6U.js";
30
+ } from "./chunk-Z43DS7XN.js";
30
31
  import {
31
32
  CredentialUnavailableError,
32
33
  buildOAuthTokenRequest,
@@ -38,6 +39,7 @@ import {
38
39
  getPluginMcpProviders,
39
40
  getPluginOAuthConfig,
40
41
  getPluginProviders,
42
+ hasRequiredOAuthScope,
41
43
  isPluginProvider,
42
44
  isRecord,
43
45
  logError,
@@ -55,15 +57,16 @@ import {
55
57
  toOptionalString,
56
58
  withContext,
57
59
  withSpan
58
- } from "./chunk-DTOS5CG4.js";
60
+ } from "./chunk-N4ICA2BC.js";
59
61
  import "./chunk-Z3YD6NHK.js";
60
62
  import {
61
- aboutPathCandidates,
62
63
  discoverInstalledPluginPackageContent,
63
64
  homeDir,
65
+ listReferenceFiles,
64
66
  setPluginPackages,
65
- soulPathCandidates
66
- } from "./chunk-RBB2MZAN.js";
67
+ soulPathCandidates,
68
+ worldPathCandidates
69
+ } from "./chunk-XPXD3FCE.js";
67
70
  import "./chunk-2KG3PWR4.js";
68
71
 
69
72
  // src/app.ts
@@ -72,9 +75,12 @@ import { Hono } from "hono";
72
75
  // src/handlers/diagnostics.ts
73
76
  import { readFileSync } from "fs";
74
77
  import path from "path";
75
- function readAboutText() {
78
+ function readDescriptionText() {
76
79
  try {
77
- const raw = readFileSync(path.join(homeDir(), "ABOUT.md"), "utf8").trim();
80
+ const raw = readFileSync(
81
+ path.join(homeDir(), "DESCRIPTION.md"),
82
+ "utf8"
83
+ ).trim();
78
84
  return raw || void 0;
79
85
  } catch {
80
86
  return void 0;
@@ -86,7 +92,7 @@ async function GET() {
86
92
  return Response.json({
87
93
  cwd: process.cwd(),
88
94
  homeDir: homeDir(),
89
- aboutText: readAboutText(),
95
+ descriptionText: readDescriptionText(),
90
96
  providers: getPluginProviders().map((plugin) => plugin.manifest.name),
91
97
  skills: skills.map((skill) => ({
92
98
  name: skill.name,
@@ -198,9 +204,9 @@ async function GET3() {
198
204
  </head>
199
205
  <body>
200
206
  <h1>&gt; junior</h1>`;
201
- if (d?.aboutText) {
207
+ if (d?.descriptionText) {
202
208
  html += `
203
- <div class="subtitle">${escapeXml(String(d.aboutText))}</div>`;
209
+ <div class="subtitle">${escapeXml(String(d.descriptionText))}</div>`;
204
210
  }
205
211
  html += `
206
212
  <div class="section">
@@ -2298,6 +2304,17 @@ async function completeObject(params) {
2298
2304
  };
2299
2305
  }
2300
2306
 
2307
+ // src/chat/slack/message.ts
2308
+ function getSlackMessageTs(message) {
2309
+ if (message.id.endsWith(":message_changed_mention") && message.raw && typeof message.raw === "object") {
2310
+ const ts = message.raw.ts;
2311
+ if (typeof ts === "string" && ts.length > 0) {
2312
+ return ts;
2313
+ }
2314
+ }
2315
+ return message.id;
2316
+ }
2317
+
2301
2318
  // src/chat/services/conversation-memory.ts
2302
2319
  var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
2303
2320
  var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
@@ -2572,7 +2589,7 @@ function createConversationMessageFromSdkMessage(entry) {
2572
2589
  isBot: typeof entry.author.isBot === "boolean" ? entry.author.isBot : void 0
2573
2590
  },
2574
2591
  meta: {
2575
- slackTs: entry.id
2592
+ slackTs: getSlackMessageTs(entry)
2576
2593
  }
2577
2594
  };
2578
2595
  }
@@ -2651,6 +2668,7 @@ import { Agent } from "@mariozechner/pi-agent-core";
2651
2668
 
2652
2669
  // src/chat/prompt.ts
2653
2670
  import fs from "fs";
2671
+ import path2 from "path";
2654
2672
  var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2655
2673
  function getLoggedMarkdownFiles() {
2656
2674
  const globalState = globalThis;
@@ -2698,8 +2716,8 @@ function loadSoul() {
2698
2716
  );
2699
2717
  return DEFAULT_SOUL;
2700
2718
  }
2701
- function loadAbout() {
2702
- return loadOptionalMarkdownFile(aboutPathCandidates(), "ABOUT.md");
2719
+ function loadWorld() {
2720
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
2703
2721
  }
2704
2722
  var JUNIOR_PERSONALITY = (() => {
2705
2723
  try {
@@ -2716,17 +2734,17 @@ var JUNIOR_PERSONALITY = (() => {
2716
2734
  return DEFAULT_SOUL;
2717
2735
  }
2718
2736
  })();
2719
- var JUNIOR_ABOUT = (() => {
2737
+ var JUNIOR_WORLD = (() => {
2720
2738
  try {
2721
- return loadAbout();
2739
+ return loadWorld();
2722
2740
  } catch (error) {
2723
2741
  logWarn(
2724
- "about_load_failed",
2742
+ "world_load_failed",
2725
2743
  {},
2726
2744
  {
2727
2745
  "error.message": error instanceof Error ? error.message : String(error)
2728
2746
  },
2729
- "Failed to load ABOUT.md; omitting about prompt context"
2747
+ "Failed to load WORLD.md; omitting world prompt context"
2730
2748
  );
2731
2749
  return null;
2732
2750
  }
@@ -2846,6 +2864,25 @@ function baseSystemPrompt() {
2846
2864
  "- When active skills are present, follow their instructions before default behavior."
2847
2865
  ].join("\n");
2848
2866
  }
2867
+ function formatReferenceFilesSection() {
2868
+ const files = listReferenceFiles();
2869
+ if (files.length === 0) {
2870
+ return [];
2871
+ }
2872
+ const fileNames = files.map((filePath) => {
2873
+ const name = path2.basename(filePath);
2874
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
2875
+ });
2876
+ return [
2877
+ renderTag(
2878
+ "reference-files",
2879
+ [
2880
+ "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
2881
+ ...fileNames
2882
+ ].join("\n")
2883
+ )
2884
+ ];
2885
+ }
2849
2886
  function buildSystemPrompt(params) {
2850
2887
  const {
2851
2888
  availableSkills,
@@ -2857,7 +2894,8 @@ function buildSystemPrompt(params) {
2857
2894
  artifactState,
2858
2895
  configuration,
2859
2896
  relevantConfigurationKeys,
2860
- runtimeMetadata
2897
+ runtimeMetadata,
2898
+ threadParticipants
2861
2899
  } = params;
2862
2900
  const assistantSection = renderIdentityBlock("assistant", {
2863
2901
  user_name: assistant?.userName ?? botConfig.userName,
@@ -2913,22 +2951,43 @@ function buildSystemPrompt(params) {
2913
2951
  JUNIOR_PERSONALITY.trim()
2914
2952
  ].join("\n")
2915
2953
  ),
2916
- ...JUNIOR_ABOUT ? [
2954
+ ...JUNIOR_WORLD ? [
2917
2955
  renderTag(
2918
- "about",
2956
+ "world",
2919
2957
  [
2920
- "Use this as the assistant's product/domain description when relevant.",
2958
+ "Use this as the assistant's operational/domain context.",
2921
2959
  "",
2922
- JUNIOR_ABOUT.trim()
2960
+ JUNIOR_WORLD.trim()
2923
2961
  ].join("\n")
2924
2962
  )
2925
2963
  ] : [],
2964
+ ...formatReferenceFilesSection(),
2926
2965
  renderTag(
2927
2966
  "identity-context",
2928
2967
  [
2929
2968
  "Use these blocks as authoritative metadata for identity questions.",
2930
2969
  assistantSection,
2931
- requesterSection
2970
+ requesterSection,
2971
+ ...threadParticipants && threadParticipants.length > 0 ? [
2972
+ renderTag(
2973
+ "thread-participants",
2974
+ [
2975
+ "Known participants in this thread. When you mention one of these people, use the provided Slack mention token exactly as `<@USERID>` and do not write a bare `@name` form.",
2976
+ ...threadParticipants.map((p) => {
2977
+ const parts = [];
2978
+ if (p.userId) {
2979
+ parts.push(`user_id: ${escapeXml(p.userId)}`);
2980
+ parts.push(`slack_mention: <@${p.userId}>`);
2981
+ }
2982
+ if (p.userName)
2983
+ parts.push(`user_name: ${escapeXml(p.userName)}`);
2984
+ if (p.fullName)
2985
+ parts.push(`full_name: ${escapeXml(p.fullName)}`);
2986
+ return `- ${parts.join(", ")}`;
2987
+ })
2988
+ ].join("\n")
2989
+ )
2990
+ ] : []
2932
2991
  ].join("\n")
2933
2992
  ),
2934
2993
  renderTag(
@@ -3827,7 +3886,8 @@ async function handleOAuthStartCommand(args, deps) {
3827
3886
  }
3828
3887
  if (deps.requesterId && deps.userTokenStore) {
3829
3888
  const stored = await deps.userTokenStore.get(deps.requesterId, provider);
3830
- if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now())) {
3889
+ const providerConfig = getPluginOAuthConfig(provider);
3890
+ if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) && hasRequiredOAuthScope(stored.scope, providerConfig?.scope)) {
3831
3891
  const providerLabel = formatProviderLabel(provider);
3832
3892
  return commandResult({
3833
3893
  stdout: {
@@ -3982,12 +4042,12 @@ async function maybeExecuteJrRpcCustomCommand(command, deps) {
3982
4042
 
3983
4043
  // src/chat/sandbox/skill-sandbox.ts
3984
4044
  import fs2 from "fs/promises";
3985
- import path2 from "path";
4045
+ import path3 from "path";
3986
4046
  var MAX_SKILL_FILE_BYTES = 256 * 1024;
3987
4047
  var DEFAULT_MAX_SKILL_FILE_CHARS = 2e4;
3988
4048
  var DEFAULT_MAX_SKILL_LIST_ENTRIES = 200;
3989
4049
  function normalizePathForOutput(value) {
3990
- return value.split(path2.sep).join("/");
4050
+ return value.split(path3.sep).join("/");
3991
4051
  }
3992
4052
  function normalizeSkillName(value) {
3993
4053
  return value.trim().toLowerCase();
@@ -3996,12 +4056,12 @@ function resolvePathWithinRoot(root, relativePath) {
3996
4056
  if (!relativePath.trim()) {
3997
4057
  throw new Error("Path must not be empty.");
3998
4058
  }
3999
- if (path2.isAbsolute(relativePath)) {
4059
+ if (path3.isAbsolute(relativePath)) {
4000
4060
  throw new Error("Absolute paths are not allowed.");
4001
4061
  }
4002
- const resolvedRoot = path2.resolve(root);
4003
- const resolvedPath = path2.resolve(resolvedRoot, relativePath);
4004
- if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path2.sep}`)) {
4062
+ const resolvedRoot = path3.resolve(root);
4063
+ const resolvedPath = path3.resolve(resolvedRoot, relativePath);
4064
+ if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path3.sep}`)) {
4005
4065
  throw new Error("Path escapes the skill directory.");
4006
4066
  }
4007
4067
  return resolvedPath;
@@ -4081,7 +4141,7 @@ var SkillSandbox = class {
4081
4141
  1,
4082
4142
  Math.min(params.maxEntries ?? DEFAULT_MAX_SKILL_LIST_ENTRIES, 1e3)
4083
4143
  );
4084
- const root = path2.resolve(skill.skillPath);
4144
+ const root = path3.resolve(skill.skillPath);
4085
4145
  const targetDirectory = resolvePathWithinRoot(root, directory);
4086
4146
  const targetStats = await fs2.stat(targetDirectory);
4087
4147
  if (!targetStats.isDirectory()) {
@@ -4097,9 +4157,9 @@ var SkillSandbox = class {
4097
4157
  });
4098
4158
  children.sort((a, b) => a.name.localeCompare(b.name));
4099
4159
  for (const child of children) {
4100
- const absolutePath = path2.join(currentDirectory, child.name);
4160
+ const absolutePath = path3.join(currentDirectory, child.name);
4101
4161
  const relativePath = normalizePathForOutput(
4102
- path2.relative(root, absolutePath)
4162
+ path3.relative(root, absolutePath)
4103
4163
  );
4104
4164
  if (!relativePath || relativePath.startsWith("..")) {
4105
4165
  continue;
@@ -4122,7 +4182,7 @@ var SkillSandbox = class {
4122
4182
  }
4123
4183
  }
4124
4184
  const relativeDirectory = normalizePathForOutput(
4125
- path2.relative(root, targetDirectory) || "."
4185
+ path3.relative(root, targetDirectory) || "."
4126
4186
  );
4127
4187
  return {
4128
4188
  skillName: skill.name,
@@ -4137,7 +4197,7 @@ var SkillSandbox = class {
4137
4197
  1,
4138
4198
  Math.min(params.maxChars ?? DEFAULT_MAX_SKILL_FILE_CHARS, 1e5)
4139
4199
  );
4140
- const root = path2.resolve(skill.skillPath);
4200
+ const root = path3.resolve(skill.skillPath);
4141
4201
  const targetPath = resolvePathWithinRoot(root, params.filePath);
4142
4202
  const stats = await fs2.stat(targetPath);
4143
4203
  if (!stats.isFile()) {
@@ -4152,7 +4212,7 @@ var SkillSandbox = class {
4152
4212
  const truncated = raw.length > maxChars;
4153
4213
  return {
4154
4214
  skillName: skill.name,
4155
- path: normalizePathForOutput(path2.relative(root, targetPath)),
4215
+ path: normalizePathForOutput(path3.relative(root, targetPath)),
4156
4216
  content: truncated ? raw.slice(0, maxChars) : raw,
4157
4217
  truncated
4158
4218
  };
@@ -4726,7 +4786,7 @@ function createBashTool() {
4726
4786
  }
4727
4787
 
4728
4788
  // src/chat/tools/sandbox/attach-file.ts
4729
- import path3 from "path";
4789
+ import path4 from "path";
4730
4790
  import { Type as Type2 } from "@sinclair/typebox";
4731
4791
  var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
4732
4792
  var MIME_BY_EXTENSION = {
@@ -4748,20 +4808,20 @@ function normalizeSandboxPath(inputPath) {
4748
4808
  if (!trimmed) {
4749
4809
  throw new Error("path is required");
4750
4810
  }
4751
- if (path3.posix.isAbsolute(trimmed)) {
4811
+ if (path4.posix.isAbsolute(trimmed)) {
4752
4812
  return trimmed;
4753
4813
  }
4754
- return path3.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
4814
+ return path4.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
4755
4815
  }
4756
4816
  function sanitizeFilename(value, fallbackPath) {
4757
4817
  const candidate = (value ?? "").trim();
4758
4818
  if (candidate) {
4759
- const base = path3.posix.basename(candidate);
4819
+ const base = path4.posix.basename(candidate);
4760
4820
  if (base && base !== "." && base !== "..") {
4761
4821
  return base;
4762
4822
  }
4763
4823
  }
4764
- const derived = path3.posix.basename(fallbackPath);
4824
+ const derived = path4.posix.basename(fallbackPath);
4765
4825
  if (derived && derived !== "." && derived !== "..") {
4766
4826
  return derived;
4767
4827
  }
@@ -4772,7 +4832,7 @@ function inferMimeType(filename, explicitMimeType) {
4772
4832
  if (explicit) {
4773
4833
  return explicit;
4774
4834
  }
4775
- const ext = path3.extname(filename).toLowerCase();
4835
+ const ext = path4.extname(filename).toLowerCase();
4776
4836
  return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
4777
4837
  }
4778
4838
  async function detectMimeType(sandbox, targetPath) {
@@ -4819,7 +4879,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
4819
4879
  const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
4820
4880
  if (!fileBuffer) {
4821
4881
  const generatedFile = hooks.getGeneratedFile?.(
4822
- path3.posix.basename(targetPath)
4882
+ path4.posix.basename(targetPath)
4823
4883
  );
4824
4884
  if (generatedFile) {
4825
4885
  hooks.onGeneratedFiles?.([generatedFile]);
@@ -5021,7 +5081,7 @@ function toLoadedSkill(result, availableSkills) {
5021
5081
  return {
5022
5082
  name: result.skill_name,
5023
5083
  description: result.description,
5024
- skillPath: result.skill_dir,
5084
+ skillPath: metadata?.skillPath ?? result.skill_dir,
5025
5085
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
5026
5086
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
5027
5087
  ...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
@@ -5029,7 +5089,7 @@ function toLoadedSkill(result, availableSkills) {
5029
5089
  body: result.instructions
5030
5090
  };
5031
5091
  }
5032
- async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5092
+ async function loadSkillFromHost(availableSkills, skillName) {
5033
5093
  const requested = skillName.trim().toLowerCase();
5034
5094
  const skill = availableSkills.find(
5035
5095
  (entry) => entry.name.toLowerCase() === requested
@@ -5042,10 +5102,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5042
5102
  };
5043
5103
  }
5044
5104
  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}`);
5105
+ const skillFilePath = sandboxSkillFile(skill.name);
5106
+ const [loaded] = await loadSkillsByName([skill.name], availableSkills);
5107
+ if (!loaded) {
5108
+ throw new Error(`failed to load ${skill.name}`);
5049
5109
  }
5050
5110
  return {
5051
5111
  ok: true,
@@ -5054,10 +5114,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5054
5114
  ...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
5055
5115
  skill_dir: skillDir,
5056
5116
  location: skillFilePath,
5057
- instructions: stripFrontmatter(file.toString("utf8"))
5117
+ instructions: loaded.body
5058
5118
  };
5059
5119
  }
5060
- function createLoadSkillTool(sandbox, availableSkills, options) {
5120
+ function createLoadSkillTool(availableSkills, options) {
5061
5121
  return tool({
5062
5122
  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
5123
  inputSchema: Type4.Object({
@@ -5067,11 +5127,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
5067
5127
  })
5068
5128
  }),
5069
5129
  execute: async ({ skill_name }) => {
5070
- const result = await loadSkillFromSandbox(
5071
- sandbox,
5072
- availableSkills,
5073
- skill_name
5074
- );
5130
+ const result = await loadSkillFromHost(availableSkills, skill_name);
5075
5131
  const loadedSkill = toLoadedSkill(result, availableSkills);
5076
5132
  if (loadedSkill) {
5077
5133
  const metadata = await options?.onSkillLoaded?.(loadedSkill);
@@ -6810,7 +6866,7 @@ function createToolState(hooks, context) {
6810
6866
  function createTools(availableSkills, hooks = {}, context) {
6811
6867
  const state = createToolState(hooks, context);
6812
6868
  const tools = {
6813
- loadSkill: createLoadSkillTool(context.sandbox, availableSkills, {
6869
+ loadSkill: createLoadSkillTool(availableSkills, {
6814
6870
  onSkillLoaded: hooks.onSkillLoaded
6815
6871
  }),
6816
6872
  systemTime: createSystemTimeTool(),
@@ -6866,10 +6922,7 @@ function resolveChannelCapabilities(channelId) {
6866
6922
  }
6867
6923
 
6868
6924
  // 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";
6925
+ import fs4 from "fs/promises";
6873
6926
 
6874
6927
  // src/chat/sandbox/http-error-details.ts
6875
6928
  var DEFAULT_PREVIEW_LIMIT = 512;
@@ -6973,6 +7026,109 @@ function extractHttpErrorDetails(error, options = {}) {
6973
7026
  };
6974
7027
  }
6975
7028
 
7029
+ // src/chat/sandbox/errors.ts
7030
+ var SANDBOX_ERROR_FIELDS = [
7031
+ {
7032
+ sourceKey: "sandboxId",
7033
+ attributeKey: "sandbox_id",
7034
+ summaryKey: "sandboxId"
7035
+ }
7036
+ ];
7037
+ function getSandboxErrorDetails(error) {
7038
+ return extractHttpErrorDetails(error, {
7039
+ attributePrefix: "app.sandbox.api_error",
7040
+ extraFields: [...SANDBOX_ERROR_FIELDS]
7041
+ });
7042
+ }
7043
+ function findInErrorChain(error, predicate) {
7044
+ const seen = /* @__PURE__ */ new Set();
7045
+ let current = error;
7046
+ while (current && !seen.has(current)) {
7047
+ if (predicate(current)) {
7048
+ return true;
7049
+ }
7050
+ seen.add(current);
7051
+ current = typeof current === "object" ? current.cause : void 0;
7052
+ }
7053
+ return false;
7054
+ }
7055
+ function getFirstErrorMessage(error) {
7056
+ const seen = /* @__PURE__ */ new Set();
7057
+ let current = error;
7058
+ while (current && !seen.has(current)) {
7059
+ if (current instanceof Error) {
7060
+ const message = current.message.trim();
7061
+ if (message) {
7062
+ return message;
7063
+ }
7064
+ }
7065
+ seen.add(current);
7066
+ current = typeof current === "object" ? current.cause : void 0;
7067
+ }
7068
+ return void 0;
7069
+ }
7070
+ function isAlreadyExistsError(error) {
7071
+ const details = getSandboxErrorDetails(error);
7072
+ return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7073
+ }
7074
+ function isSandboxUnavailableError(error) {
7075
+ return findInErrorChain(error, (candidate) => {
7076
+ const details = getSandboxErrorDetails(candidate);
7077
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7078
+ return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7079
+ });
7080
+ }
7081
+ function isSnapshottingError(error) {
7082
+ return findInErrorChain(error, (candidate) => {
7083
+ const details = getSandboxErrorDetails(candidate);
7084
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7085
+ return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7086
+ });
7087
+ }
7088
+ function wrapSandboxSetupError(error) {
7089
+ try {
7090
+ const details = getSandboxErrorDetails(error);
7091
+ if (details.summary) {
7092
+ return new Error(`sandbox setup failed (${details.summary})`, {
7093
+ cause: error
7094
+ });
7095
+ }
7096
+ } catch {
7097
+ }
7098
+ let causeMessage;
7099
+ try {
7100
+ causeMessage = getFirstErrorMessage(error);
7101
+ } catch (cause) {
7102
+ causeMessage = cause instanceof Error ? cause.message : void 0;
7103
+ }
7104
+ if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7105
+ const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7106
+ return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7107
+ }
7108
+ return new Error("sandbox setup failed", { cause: error });
7109
+ }
7110
+ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7111
+ const details = getSandboxErrorDetails(error);
7112
+ setSpanAttributes({
7113
+ ...details.attributes,
7114
+ ...includeMissingPath ? {
7115
+ "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7116
+ } : {},
7117
+ "app.sandbox.success": false
7118
+ });
7119
+ setSpanStatus("error");
7120
+ throw new Error(
7121
+ details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7122
+ {
7123
+ cause: error
7124
+ }
7125
+ );
7126
+ }
7127
+
7128
+ // src/chat/sandbox/session.ts
7129
+ import { Sandbox } from "@vercel/sandbox";
7130
+ import { createBashTool as createBashTool2 } from "bash-tool";
7131
+
6976
7132
  // src/chat/runtime/status-format.ts
6977
7133
  var SLACK_STATUS_MAX_LENGTH = 50;
6978
7134
  function truncateWithEllipsis(text, maxLength) {
@@ -7234,112 +7390,11 @@ function logAssistantStatusFailure(status, error) {
7234
7390
  );
7235
7391
  }
7236
7392
 
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)}
7393
+ // src/chat/sandbox/skill-sync.ts
7394
+ import fs3 from "fs/promises";
7395
+ import path5 from "path";
7282
7396
 
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
- }
7397
+ // src/chat/sandbox/eval-gh-stub.ts
7343
7398
  function buildEvalGitHubCliStub() {
7344
7399
  return `#!/usr/bin/env node
7345
7400
  const fs = require("node:fs");
@@ -7517,6 +7572,8 @@ if (args[0] === "api") {
7517
7572
  outputJson({ items: [] });
7518
7573
  process.exit(0);
7519
7574
  }
7575
+ outputJson({});
7576
+ process.exit(0);
7520
7577
  }
7521
7578
 
7522
7579
  if (args[0] === "issue") {
@@ -7558,7 +7615,9 @@ if (args[0] === "issue") {
7558
7615
 
7559
7616
  const number = Number.parseInt(positionals[2] || "", 10);
7560
7617
  const key = repo + "#" + number;
7561
- const record = state.issues[key] || defaultIssue(repo, Number.isFinite(number) ? number : 101);
7618
+ const record =
7619
+ state.issues[key] ||
7620
+ defaultIssue(repo, Number.isFinite(number) ? number : 101);
7562
7621
 
7563
7622
  if (subcommand === "view") {
7564
7623
  const jsonFields = getFlag("--json");
@@ -7599,134 +7658,379 @@ if (args[0] === "issue") {
7599
7658
  fallbackToRealGh();
7600
7659
  `;
7601
7660
  }
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);
7661
+
7662
+ // src/chat/sandbox/skill-sync.ts
7663
+ function toPosixRelative(base, absolute) {
7664
+ return path5.relative(base, absolute).split(path5.sep).join("/");
7665
+ }
7666
+ async function listFilesRecursive(root) {
7667
+ const queue = [root];
7668
+ const files = [];
7669
+ while (queue.length > 0) {
7670
+ const dir = queue.shift();
7671
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
7672
+ entries.sort((a, b) => a.name.localeCompare(b.name));
7673
+ for (const entry of entries) {
7674
+ const absolute = path5.join(dir, entry.name);
7675
+ if (entry.isDirectory()) {
7676
+ queue.push(absolute);
7677
+ } else if (entry.isFile()) {
7678
+ files.push(absolute);
7679
+ }
7611
7680
  }
7612
7681
  }
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
- });
7682
+ return files;
7622
7683
  }
7623
- function sleep2(ms) {
7624
- return new Promise((resolve) => {
7625
- setTimeout(resolve, ms);
7684
+ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFiles) {
7685
+ const filesToWrite = [];
7686
+ const index = {
7687
+ skills: []
7688
+ };
7689
+ for (const skill of availableSkills) {
7690
+ const skillFiles = await listFilesRecursive(skill.skillPath);
7691
+ for (const absoluteFile of skillFiles) {
7692
+ const relative = toPosixRelative(skill.skillPath, absoluteFile);
7693
+ if (!relative || relative.startsWith("..")) {
7694
+ continue;
7695
+ }
7696
+ filesToWrite.push({
7697
+ path: `${sandboxSkillDir(skill.name)}/${relative}`,
7698
+ content: await fs3.readFile(absoluteFile)
7699
+ });
7700
+ }
7701
+ index.skills.push({
7702
+ name: skill.name,
7703
+ description: skill.description,
7704
+ root: sandboxSkillDir(skill.name)
7705
+ });
7706
+ }
7707
+ filesToWrite.push({
7708
+ path: `${SANDBOX_SKILLS_ROOT}/index.json`,
7709
+ content: Buffer.from(JSON.stringify(index), "utf8")
7626
7710
  });
7711
+ if (referenceFiles && referenceFiles.length > 0) {
7712
+ for (const absoluteFile of referenceFiles) {
7713
+ const fileName = path5.basename(absoluteFile);
7714
+ filesToWrite.push({
7715
+ path: `${SANDBOX_DATA_ROOT}/${fileName}`,
7716
+ content: await fs3.readFile(absoluteFile)
7717
+ });
7718
+ }
7719
+ }
7720
+ if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
7721
+ filesToWrite.push({
7722
+ path: `${runtimeBinDir}/gh`,
7723
+ content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
7724
+ });
7725
+ }
7726
+ return filesToWrite;
7627
7727
  }
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");
7728
+ function collectDirectories(filesToWrite, workspaceRoot) {
7729
+ const directoriesToEnsure = /* @__PURE__ */ new Set();
7730
+ for (const file of filesToWrite) {
7731
+ const normalizedPath = path5.posix.normalize(file.path);
7732
+ const parts = normalizedPath.split("/").filter(Boolean);
7733
+ let current = "";
7734
+ for (let index = 0; index < parts.length - 1; index += 1) {
7735
+ current = `${current}/${parts[index]}`;
7736
+ directoriesToEnsure.add(current);
7737
+ }
7738
+ }
7739
+ return Array.from(directoriesToEnsure).filter(
7740
+ (directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
7741
+ ).sort((a, b) => a.length - b.length);
7631
7742
  }
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;
7743
+ function resolveHostSkillPath(availableSkills, sandboxPath) {
7744
+ const normalizedPath = path5.posix.normalize(sandboxPath.trim());
7745
+ for (const skill of availableSkills) {
7746
+ const virtualRoot = sandboxSkillDir(skill.name);
7747
+ if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
7748
+ continue;
7638
7749
  }
7639
- seen.add(current);
7640
- if (typeof current === "object") {
7641
- current = current.cause;
7642
- } else {
7643
- current = void 0;
7750
+ const relativePath = path5.posix.relative(virtualRoot, normalizedPath);
7751
+ if (!relativePath || relativePath.startsWith("../")) {
7752
+ return null;
7644
7753
  }
7754
+ const hostRoot = path5.resolve(skill.skillPath);
7755
+ const hostPath = path5.resolve(hostRoot, ...relativePath.split("/"));
7756
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path5.sep}`)) {
7757
+ return null;
7758
+ }
7759
+ return hostPath;
7645
7760
  }
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");
7660
- });
7761
+ return null;
7661
7762
  }
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
- }
7763
+ function resolveHostDataPath(referenceFiles, sandboxPath) {
7764
+ const normalizedPath = path5.posix.normalize(sandboxPath.trim());
7765
+ if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
7766
+ return null;
7767
+ }
7768
+ const relativePath = path5.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
7769
+ if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
7770
+ return null;
7771
+ }
7772
+ for (const hostFile of referenceFiles) {
7773
+ if (path5.basename(hostFile) === relativePath) {
7774
+ return hostFile;
7671
7775
  }
7672
- seen.add(current);
7673
- current = typeof current === "object" ? current.cause : void 0;
7674
7776
  }
7675
- return void 0;
7777
+ return null;
7676
7778
  }
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
- });
7779
+ function isHostFileMissingError(error) {
7780
+ return Boolean(
7781
+ error && typeof error === "object" && error.code === "ENOENT"
7782
+ );
7783
+ }
7784
+ async function syncSkillsToSandbox(params) {
7785
+ const workspaceRoot = params.workspaceRoot ?? SANDBOX_WORKSPACE_ROOT;
7786
+ await params.withSpan(
7787
+ "sandbox.sync_skills",
7788
+ "sandbox.sync",
7789
+ {
7790
+ "app.sandbox.skills_count": params.skills.length
7791
+ },
7792
+ async () => {
7793
+ const filesToWrite = await buildSkillSyncFiles(
7794
+ params.skills,
7795
+ params.runtimeBinDir,
7796
+ params.referenceFiles
7797
+ );
7798
+ const bytesWritten = filesToWrite.reduce(
7799
+ (total, file) => total + file.content.length,
7800
+ 0
7801
+ );
7802
+ const directories = collectDirectories(filesToWrite, workspaceRoot);
7803
+ await params.withSpan(
7804
+ "sandbox.sync_writeFiles",
7805
+ "sandbox.sync.write",
7806
+ {
7807
+ "app.sandbox.sync.files_written": filesToWrite.length,
7808
+ "app.sandbox.sync.bytes_written": bytesWritten,
7809
+ "app.sandbox.sync.directories_ensured": directories.length
7810
+ },
7811
+ async () => {
7812
+ try {
7813
+ for (const directory of directories) {
7814
+ try {
7815
+ await params.sandbox.mkDir(directory);
7816
+ } catch (error) {
7817
+ if (!isAlreadyExistsError(error)) {
7818
+ throw error;
7819
+ }
7820
+ }
7821
+ }
7822
+ await params.sandbox.writeFiles(filesToWrite);
7823
+ const executableFiles = filesToWrite.map((file) => file.path).filter(
7824
+ (filePath) => filePath.startsWith(`${params.runtimeBinDir}/`)
7825
+ );
7826
+ for (const filePath of executableFiles) {
7827
+ const chmod = await runNonInteractiveCommand(params.sandbox, {
7828
+ cmd: "chmod",
7829
+ args: ["0755", filePath],
7830
+ cwd: workspaceRoot
7831
+ });
7832
+ if (chmod.exitCode !== 0) {
7833
+ throw new Error(
7834
+ `sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
7835
+ );
7836
+ }
7837
+ }
7838
+ } catch (error) {
7839
+ throwSandboxOperationError("sandbox writeFiles", error, true);
7840
+ }
7841
+ }
7842
+ );
7684
7843
  }
7685
- } catch {
7686
- }
7687
- let causeMessage;
7688
- try {
7689
- causeMessage = getFirstErrorMessage(error);
7690
- } catch (cause) {
7691
- causeMessage = cause instanceof Error ? cause.message : void 0;
7844
+ );
7845
+ }
7846
+
7847
+ // src/chat/sandbox/session.ts
7848
+ var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
7849
+ var SANDBOX_RUNTIME = "node22";
7850
+ var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
7851
+ var SNAPSHOT_BOOT_RETRY_COUNT = 3;
7852
+ var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
7853
+ var SNAPSHOT_PHASE_STATUS = {
7854
+ resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
7855
+ waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
7856
+ building_snapshot: { kind: "creating", context: "sandbox snapshot" },
7857
+ cache_hit: { kind: "loading", context: "sandbox snapshot" }
7858
+ };
7859
+ function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
7860
+ const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
7861
+ const existingAllowRaw = basePolicy.allow;
7862
+ const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
7863
+ Object.entries(existingAllowRaw).map(
7864
+ ([domain, rules]) => [
7865
+ domain,
7866
+ Array.isArray(rules) ? [...rules] : []
7867
+ ]
7868
+ )
7869
+ ) : { "*": [] };
7870
+ for (const transform of headerTransforms) {
7871
+ const currentRules = existingAllow[transform.domain] ?? [];
7872
+ existingAllow[transform.domain] = [
7873
+ ...currentRules,
7874
+ { transform: [{ headers: transform.headers }] }
7875
+ ];
7692
7876
  }
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 });
7877
+ return {
7878
+ ...basePolicy,
7879
+ allow: existingAllow
7880
+ };
7881
+ }
7882
+ function truncateOutput(output, maxLength) {
7883
+ if (output.length <= maxLength) {
7884
+ return { value: output, truncated: false };
7696
7885
  }
7697
- return new Error("sandbox setup failed", { cause: error });
7886
+ const truncatedLength = output.length - maxLength;
7887
+ return {
7888
+ value: `${output.slice(0, maxLength)}
7889
+
7890
+ [output truncated: ${truncatedLength} characters removed]`,
7891
+ truncated: true
7892
+ };
7698
7893
  }
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
7894
+ function sleep2(ms) {
7895
+ return new Promise((resolve) => {
7896
+ setTimeout(resolve, ms);
7707
7897
  });
7708
- setSpanStatus("error");
7709
- throw new Error(
7710
- details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7711
- {
7712
- cause: error
7898
+ }
7899
+ function createStatusEmitter(emitStatus) {
7900
+ let statusCount = 0;
7901
+ const sentStatuses = /* @__PURE__ */ new Set();
7902
+ const emit = async (status) => {
7903
+ const statusKey = `${status.kind}:${status.context ?? ""}`;
7904
+ if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
7905
+ return;
7713
7906
  }
7907
+ sentStatuses.add(statusKey);
7908
+ statusCount += 1;
7909
+ await emitStatus(status);
7910
+ };
7911
+ const reportSnapshotPhase = async (phase) => {
7912
+ const status = SNAPSHOT_PHASE_STATUS[phase];
7913
+ if (status) {
7914
+ await emit(makeAssistantStatus(status.kind, status.context));
7915
+ }
7916
+ };
7917
+ return { emit, reportSnapshotPhase };
7918
+ }
7919
+ function parseKeepAliveMs() {
7920
+ const parsed = Number.parseInt(
7921
+ process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
7922
+ 10
7714
7923
  );
7924
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
7715
7925
  }
7716
- function createSandboxExecutor(options) {
7926
+ function createSandboxSessionManager(options) {
7717
7927
  let sandbox = null;
7718
7928
  let sandboxIdHint = options?.sandboxId;
7719
7929
  let availableSkills = [];
7930
+ let availableReferenceFiles = [];
7720
7931
  let toolExecutors;
7721
7932
  const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
7722
7933
  const traceContext = options?.traceContext ?? {};
7723
- const emitStatus = options?.onStatus;
7724
7934
  const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
7725
7935
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
7726
- const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, onStatus) => {
7936
+ const emitSandboxStatus = async (source, statusEmitter, status) => {
7937
+ logInfo(
7938
+ "sandbox_status_emitted",
7939
+ traceContext,
7940
+ {
7941
+ "app.sandbox.status.source": source,
7942
+ "app.sandbox.status.kind": status.kind,
7943
+ ...status.context ? { "app.sandbox.status.context": status.context } : {}
7944
+ },
7945
+ "Sandbox status emitted"
7946
+ );
7947
+ if (typeof statusEmitter === "function") {
7948
+ await statusEmitter(status);
7949
+ return;
7950
+ }
7951
+ await statusEmitter.emit(status);
7952
+ };
7953
+ const clearSession = () => {
7954
+ sandbox = null;
7955
+ sandboxIdHint = void 0;
7956
+ toolExecutors = void 0;
7957
+ };
7958
+ const rememberSandbox = (nextSandbox) => {
7959
+ sandbox = nextSandbox;
7960
+ sandboxIdHint = nextSandbox.sandboxId;
7961
+ toolExecutors = void 0;
7962
+ return nextSandbox;
7963
+ };
7964
+ const failSetup = (error) => {
7965
+ throw wrapSandboxSetupError(error);
7966
+ };
7967
+ const syncSkills = async (targetSandbox) => {
7968
+ await syncSkillsToSandbox({
7969
+ sandbox: targetSandbox,
7970
+ skills: availableSkills,
7971
+ referenceFiles: availableReferenceFiles,
7972
+ withSpan: withSandboxSpan,
7973
+ runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
7974
+ });
7975
+ };
7976
+ const ensureSandboxReachable = async (targetSandbox, source) => {
7977
+ await withSandboxSpan(
7978
+ "sandbox.reuse_probe",
7979
+ "sandbox.acquire.probe",
7980
+ {
7981
+ "app.sandbox.reused": true,
7982
+ "app.sandbox.source": source
7983
+ },
7984
+ async () => {
7985
+ try {
7986
+ await targetSandbox.mkDir(SANDBOX_WORKSPACE_ROOT);
7987
+ } catch (error) {
7988
+ if (!isAlreadyExistsError(error)) {
7989
+ throw error;
7990
+ }
7991
+ }
7992
+ }
7993
+ );
7994
+ };
7995
+ const invalidateSandboxInstance = async (targetSandbox, reason) => {
7996
+ if (sandbox === targetSandbox) {
7997
+ clearSession();
7998
+ }
7999
+ logWarn(
8000
+ "sandbox_network_policy_restore_failed",
8001
+ traceContext,
8002
+ {
8003
+ "error.message": reason instanceof Error ? reason.message : String(reason)
8004
+ },
8005
+ "Sandbox network policy restore failed; discarding sandbox instance"
8006
+ );
8007
+ try {
8008
+ await targetSandbox.stop({ blocking: true });
8009
+ } catch {
8010
+ }
8011
+ };
8012
+ const recreateUnavailableSandbox = async (source) => {
8013
+ setSpanAttributes({
8014
+ "app.sandbox.recovery.attempted": true,
8015
+ "app.sandbox.recovery.source": source
8016
+ });
8017
+ clearSession();
8018
+ const replacement = await createFreshSandbox();
8019
+ setSpanAttributes({
8020
+ "app.sandbox.recovery.succeeded": true
8021
+ });
8022
+ return replacement;
8023
+ };
8024
+ const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
7727
8025
  for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
7728
8026
  try {
7729
- await onStatus?.(makeAssistantStatus("loading", "sandbox"));
8027
+ if (emitStatus) {
8028
+ await emitSandboxStatus(
8029
+ "snapshot_boot",
8030
+ emitStatus,
8031
+ makeAssistantStatus("loading", "sandbox")
8032
+ );
8033
+ }
7730
8034
  return await Sandbox.create({
7731
8035
  timeout: timeoutMs,
7732
8036
  source: {
@@ -7744,309 +8048,265 @@ function createSandboxExecutor(options) {
7744
8048
  }
7745
8049
  throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
7746
8050
  };
7747
- const invalidateSandboxInstance = async (targetSandbox, reason) => {
7748
- if (sandbox === targetSandbox) {
7749
- sandbox = null;
7750
- sandboxIdHint = void 0;
7751
- toolExecutors = void 0;
8051
+ const setSnapshotAttributes = (snapshot) => {
8052
+ setSpanAttributes({
8053
+ "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
8054
+ "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
8055
+ "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
8056
+ ...snapshot.profileHash ? {
8057
+ "app.sandbox.snapshot.profile_hash": snapshot.profileHash
8058
+ } : {},
8059
+ "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
8060
+ ...snapshot.rebuildReason ? {
8061
+ "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
8062
+ } : {}
8063
+ });
8064
+ };
8065
+ const createSandboxFromResolvedSnapshot = async (params) => {
8066
+ const { runtime, snapshot, sandboxCredentials, status } = params;
8067
+ if (!snapshot.snapshotId) {
8068
+ await emitSandboxStatus(
8069
+ "fresh_runtime_boot",
8070
+ status,
8071
+ makeAssistantStatus("loading", "sandbox")
8072
+ );
8073
+ return await Sandbox.create({
8074
+ timeout: timeoutMs,
8075
+ runtime,
8076
+ ...sandboxCredentials ?? {}
8077
+ });
7752
8078
  }
7753
- logWarn(
7754
- "sandbox_network_policy_restore_failed",
7755
- traceContext,
7756
- {
7757
- "error.message": reason instanceof Error ? reason.message : String(reason)
7758
- },
7759
- "Sandbox network policy restore failed; discarding sandbox instance"
7760
- );
7761
8079
  try {
7762
- await targetSandbox.stop({ blocking: true });
7763
- } catch {
7764
- }
7765
- };
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
7787
- },
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
- );
8080
+ return await createSandboxFromSnapshot(
8081
+ snapshot.snapshotId,
8082
+ sandboxCredentials,
8083
+ status.emit
8084
+ );
8085
+ } catch (error) {
8086
+ if (!isSnapshotMissingError(error)) {
8087
+ throw error;
7820
8088
  }
7821
- );
8089
+ setSpanAttributes({
8090
+ "app.sandbox.snapshot.rebuild_after_missing": true
8091
+ });
8092
+ const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
8093
+ runtime,
8094
+ timeoutMs,
8095
+ forceRebuild: true,
8096
+ staleSnapshotId: snapshot.snapshotId,
8097
+ onProgress: status.reportSnapshotPhase
8098
+ });
8099
+ if (!rebuiltSnapshot.snapshotId) {
8100
+ throw error;
8101
+ }
8102
+ return await createSandboxFromSnapshot(
8103
+ rebuiltSnapshot.snapshotId,
8104
+ sandboxCredentials,
8105
+ status.emit
8106
+ );
8107
+ }
7822
8108
  };
7823
- const acquireSandbox = async () => {
7824
- return withSandboxSpan(
7825
- "sandbox.acquire",
7826
- "sandbox.acquire",
7827
- {
7828
- "app.sandbox.id_hint_present": Boolean(sandboxIdHint),
7829
- "app.sandbox.timeout_ms": timeoutMs,
7830
- "app.sandbox.runtime": "node22",
7831
- "app.sandbox.skills_count": availableSkills.length
7832
- },
7833
- 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
- }
8109
+ const createFreshSandbox = async () => {
8110
+ const runtime = SANDBOX_RUNTIME;
8111
+ const sandboxCredentials = getVercelSandboxCredentials();
8112
+ const status = createStatusEmitter(options?.onStatus);
8113
+ let createdSandbox;
8114
+ try {
8115
+ createdSandbox = await withSandboxSpan(
8116
+ "sandbox.create",
8117
+ "sandbox.create",
8118
+ {
8119
+ "app.sandbox.reused": false,
8120
+ "app.sandbox.timeout_ms": timeoutMs,
8121
+ "app.sandbox.runtime": runtime
8122
+ },
8123
+ async () => {
8124
+ await emitSandboxStatus(
8125
+ "runtime_dependency_resolve",
8126
+ status,
8127
+ makeAssistantStatus("loading", "sandbox runtime")
8128
+ );
8129
+ const snapshot = await resolveRuntimeDependencySnapshot({
8130
+ runtime,
8131
+ timeoutMs,
8132
+ onProgress: status.reportSnapshotPhase
8133
+ });
8134
+ setSnapshotAttributes(snapshot);
8135
+ return await createSandboxFromResolvedSnapshot({
8136
+ runtime,
8137
+ snapshot,
8138
+ sandboxCredentials,
8139
+ status
8140
+ });
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
+ );
8143
+ } catch (error) {
8144
+ return failSetup(error);
8145
+ }
8146
+ try {
8147
+ await syncSkills(createdSandbox);
8148
+ } catch (error) {
8149
+ return failSetup(error);
8150
+ }
8151
+ return rememberSandbox(createdSandbox);
8152
+ };
8153
+ const discardHintIfProfileChanged = () => {
8154
+ if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
8155
+ return;
8156
+ }
8157
+ setSpanAttributes({
8158
+ "app.sandbox.reused": false,
8159
+ "app.sandbox.recreate.reason": "dependency_profile_mismatch",
8160
+ ...options?.sandboxDependencyProfileHash ? {
8161
+ "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
8162
+ } : {},
8163
+ ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8164
+ });
8165
+ sandboxIdHint = void 0;
8166
+ };
8167
+ const tryReuseCachedSandbox = async () => {
8168
+ const cachedSandbox = sandbox;
8169
+ if (!cachedSandbox) {
8170
+ return null;
8171
+ }
8172
+ try {
8173
+ await ensureSandboxReachable(cachedSandbox, "memory");
8174
+ return cachedSandbox;
8175
+ } catch (error) {
8176
+ if (isSandboxUnavailableError(error)) {
8177
+ return await recreateUnavailableSandbox("memory");
8178
+ }
8179
+ return failSetup(error);
8180
+ }
8181
+ };
8182
+ const tryRestoreHintedSandbox = async () => {
8183
+ if (!sandboxIdHint) {
8184
+ return null;
8185
+ }
8186
+ let hintedSandbox = null;
8187
+ try {
8188
+ const sandboxCredentials = getVercelSandboxCredentials();
8189
+ hintedSandbox = await withSandboxSpan(
8190
+ "sandbox.get",
8191
+ "sandbox.get",
8192
+ {
8193
+ "app.sandbox.reused": true,
8194
+ "app.sandbox.source": "id_hint"
8195
+ },
8196
+ async () => await Sandbox.get({
8197
+ sandboxId: sandboxIdHint,
8198
+ ...sandboxCredentials ?? {}
8199
+ })
8200
+ );
8201
+ } catch {
8202
+ return null;
8203
+ }
8204
+ try {
8205
+ await syncSkills(hintedSandbox);
8206
+ return rememberSandbox(hintedSandbox);
8207
+ } catch (error) {
8208
+ if (isSandboxUnavailableError(error)) {
8209
+ return await recreateUnavailableSandbox("id_hint");
8210
+ }
8211
+ return failSetup(error);
8212
+ }
8213
+ };
8214
+ const acquireSandbox = async () => {
8215
+ return await withSandboxSpan(
8216
+ "sandbox.acquire",
8217
+ "sandbox.acquire",
8218
+ {
8219
+ "app.sandbox.id_hint_present": Boolean(sandboxIdHint),
8220
+ "app.sandbox.timeout_ms": timeoutMs,
8221
+ "app.sandbox.runtime": SANDBOX_RUNTIME,
8222
+ "app.sandbox.skills_count": availableSkills.length
8223
+ },
8224
+ async () => {
8225
+ discardHintIfProfileChanged();
8226
+ const cachedSandbox = await tryReuseCachedSandbox();
8227
+ if (cachedSandbox) {
8228
+ return cachedSandbox;
8029
8229
  }
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
- }
8230
+ const hintedSandbox = await tryRestoreHintedSandbox();
8231
+ if (hintedSandbox) {
8232
+ return hintedSandbox;
8040
8233
  }
8041
- return createFreshSandbox();
8234
+ return await createFreshSandbox();
8042
8235
  }
8043
8236
  );
8044
8237
  };
8045
- const getToolExecutors = async () => {
8046
- if (toolExecutors) {
8047
- return toolExecutors;
8238
+ const getMaxOutputLength = () => {
8239
+ const maxOutputLength = Number.parseInt(
8240
+ process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
8241
+ 10
8242
+ );
8243
+ return Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
8244
+ };
8245
+ const readCommandOutput = async (commandResult2) => {
8246
+ const boundedOutputLength = getMaxOutputLength();
8247
+ const stdoutRaw = await commandResult2.stdout();
8248
+ const stderrRaw = await commandResult2.stderr();
8249
+ const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
8250
+ const stderr = truncateOutput(stderrRaw, boundedOutputLength);
8251
+ return {
8252
+ stdout: stdout.value,
8253
+ stderr: stderr.value,
8254
+ exitCode: commandResult2.exitCode,
8255
+ stdoutTruncated: stdout.truncated,
8256
+ stderrTruncated: stderr.truncated
8257
+ };
8258
+ };
8259
+ const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
8260
+ if (!headerTransforms || headerTransforms.length === 0) {
8261
+ return await callback();
8262
+ }
8263
+ const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
8264
+ const policy = mergeNetworkPolicyWithHeaderTransforms(
8265
+ restoreNetworkPolicy,
8266
+ headerTransforms
8267
+ );
8268
+ await sandboxInstance.updateNetworkPolicy(policy);
8269
+ let callbackError;
8270
+ let restoreError;
8271
+ let result;
8272
+ try {
8273
+ result = await callback();
8274
+ } catch (error) {
8275
+ callbackError = error;
8276
+ throw error;
8277
+ } finally {
8278
+ try {
8279
+ await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
8280
+ } catch (error) {
8281
+ restoreError = error;
8282
+ await invalidateSandboxInstance(sandboxInstance, error);
8283
+ }
8048
8284
  }
8049
- const activeSandbox = await acquireSandbox();
8285
+ if (restoreError && !callbackError) {
8286
+ throw restoreError;
8287
+ }
8288
+ return result;
8289
+ };
8290
+ const extendKeepAlive = async (activeSandbox) => {
8291
+ const keepAliveMs = parseKeepAliveMs();
8292
+ if (keepAliveMs === 0) {
8293
+ return;
8294
+ }
8295
+ try {
8296
+ await withSandboxSpan(
8297
+ "sandbox.keepalive.extend",
8298
+ "sandbox.keepalive",
8299
+ {
8300
+ "app.sandbox.keepalive_ms": keepAliveMs
8301
+ },
8302
+ async () => {
8303
+ await activeSandbox.extendTimeout(keepAliveMs);
8304
+ }
8305
+ );
8306
+ } catch {
8307
+ }
8308
+ };
8309
+ const buildToolExecutors = async (sandboxInstance) => {
8050
8310
  const toolkit = await withSandboxSpan(
8051
8311
  "sandbox.bash_tool.init",
8052
8312
  "sandbox.tool.init",
@@ -8054,8 +8314,8 @@ function createSandboxExecutor(options) {
8054
8314
  "app.sandbox.tool_name": "bash",
8055
8315
  "app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
8056
8316
  },
8057
- async () => createBashTool2({
8058
- sandbox: activeSandbox,
8317
+ async () => await createBashTool2({
8318
+ sandbox: sandboxInstance,
8059
8319
  destination: SANDBOX_WORKSPACE_ROOT
8060
8320
  })
8061
8321
  );
@@ -8064,63 +8324,24 @@ function createSandboxExecutor(options) {
8064
8324
  if (!executeReadFile || !executeWriteFile) {
8065
8325
  throw new Error("bash-tool did not return executable tool handlers");
8066
8326
  }
8067
- toolExecutors = {
8327
+ return {
8068
8328
  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
8329
  const script = buildNonInteractiveShellScript(input.command, {
8079
8330
  env: input.env,
8080
8331
  pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
8081
8332
  });
8082
- let commandError;
8083
- let result;
8084
- let restoreError;
8085
- 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
- };
8107
- } 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
- }
8333
+ return await withTemporaryHeaderTransforms(
8334
+ sandboxInstance,
8335
+ input.headerTransforms,
8336
+ async () => {
8337
+ const commandResult2 = await sandboxInstance.runCommand({
8338
+ cmd: "bash",
8339
+ args: ["-c", script],
8340
+ cwd: SANDBOX_WORKSPACE_ROOT
8341
+ });
8342
+ return await readCommandOutput(commandResult2);
8118
8343
  }
8119
- }
8120
- if (restoreError && !commandError) {
8121
- throw restoreError;
8122
- }
8123
- return result;
8344
+ );
8124
8345
  },
8125
8346
  readFile: async (input) => await executeReadFile(input, {
8126
8347
  toolCallId: "sandbox-read-file",
@@ -8131,210 +8352,319 @@ function createSandboxExecutor(options) {
8131
8352
  messages: []
8132
8353
  })
8133
8354
  };
8134
- return toolExecutors;
8135
8355
  };
8136
- const execute = async (params) => {
8137
- const rawInput = params.input ?? {};
8138
- const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
8139
- if (params.toolName === "bash") {
8140
- if (!bashCommand) {
8141
- throw new Error("command is required");
8142
- }
8143
- if (options?.runBashCustomCommand) {
8144
- const custom = await options.runBashCustomCommand(bashCommand);
8145
- if (custom.handled) {
8146
- return { result: custom.result };
8147
- }
8148
- }
8149
- }
8356
+ const ensureReadySandbox = async () => {
8150
8357
  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
- }
8358
+ await extendKeepAlive(activeSandbox);
8359
+ return activeSandbox;
8360
+ };
8361
+ const loadToolExecutors = async (activeSandbox) => {
8362
+ if (toolExecutors) {
8363
+ return toolExecutors;
8169
8364
  }
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",
8365
+ toolExecutors = await buildToolExecutors(activeSandbox);
8366
+ return toolExecutors;
8367
+ };
8368
+ return {
8369
+ configureSkills(skills) {
8370
+ availableSkills = [...skills];
8371
+ },
8372
+ configureReferenceFiles(files) {
8373
+ availableReferenceFiles = [...files];
8374
+ },
8375
+ getSandboxId() {
8376
+ return sandbox?.sandboxId ?? sandboxIdHint;
8377
+ },
8378
+ getDependencyProfileHash() {
8379
+ return dependencyProfileHash;
8380
+ },
8381
+ async createSandbox() {
8382
+ return await acquireSandbox();
8383
+ },
8384
+ async ensureToolExecutors() {
8385
+ return await loadToolExecutors(await ensureReadySandbox());
8386
+ },
8387
+ async dispose() {
8388
+ const activeSandbox = sandbox;
8389
+ if (!activeSandbox) {
8390
+ return;
8391
+ }
8392
+ await withSandboxSpan(
8393
+ "sandbox.stop",
8394
+ "sandbox.stop",
8193
8395
  {
8194
- "process.executable.name": "bash"
8396
+ "app.sandbox.stop.blocking": true
8195
8397
  },
8196
8398
  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
- }
8399
+ await activeSandbox.stop({ blocking: true });
8224
8400
  }
8225
8401
  );
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
- };
8402
+ sandbox = null;
8403
+ toolExecutors = void 0;
8240
8404
  }
8241
- 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 ?? "");
8405
+ };
8406
+ }
8407
+
8408
+ // src/chat/sandbox/sandbox.ts
8409
+ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
8410
+ function parseHeaderTransforms(raw) {
8411
+ if (!Array.isArray(raw)) {
8412
+ return void 0;
8413
+ }
8414
+ return raw.filter(
8415
+ (value) => Boolean(value && typeof value === "object")
8416
+ ).map((transform) => ({
8417
+ domain: String(transform.domain ?? "").trim(),
8418
+ headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
8419
+ Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8420
+ ) : {}
8421
+ })).filter(
8422
+ (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
8423
+ );
8424
+ }
8425
+ function parseEnv(raw) {
8426
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8427
+ return void 0;
8428
+ }
8429
+ return Object.fromEntries(
8430
+ Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8431
+ );
8432
+ }
8433
+ function createSandboxWorkspace(sandbox) {
8434
+ return {
8435
+ sandboxId: sandbox.sandboxId,
8436
+ readFileToBuffer(input) {
8437
+ return sandbox.readFileToBuffer(input);
8438
+ },
8439
+ runCommand(input) {
8440
+ return sandbox.runCommand(input);
8441
+ }
8442
+ };
8443
+ }
8444
+ function createSandboxExecutor(options) {
8445
+ let availableSkills = [];
8446
+ let referenceFiles = [];
8447
+ const traceContext = options?.traceContext ?? {};
8448
+ const sessionManager = createSandboxSessionManager({
8449
+ sandboxId: options?.sandboxId,
8450
+ sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8451
+ timeoutMs: options?.timeoutMs,
8452
+ traceContext,
8453
+ onStatus: options?.onStatus
8454
+ });
8455
+ const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8456
+ const logSandboxBootRequest = (trigger, details = {}) => {
8457
+ if (sessionManager.getSandboxId()) {
8458
+ return;
8459
+ }
8460
+ logInfo(
8461
+ "sandbox_boot_requested",
8462
+ traceContext,
8463
+ {
8464
+ "app.sandbox.boot.trigger": trigger,
8465
+ ...details
8466
+ },
8467
+ "Sandbox boot requested"
8468
+ );
8469
+ };
8470
+ const executeBashTool = async (rawInput, command) => {
8471
+ const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
8472
+ const env = parseEnv(rawInput.env);
8473
+ logSandboxBootRequest("tool.bash", {
8474
+ "app.sandbox.command_length": command.length
8475
+ });
8476
+ const executeBash = (await sessionManager.ensureToolExecutors()).bash;
8477
+ const result = await withSandboxSpan(
8478
+ "bash",
8479
+ "process.exec",
8480
+ {
8481
+ "process.executable.name": "bash"
8482
+ },
8483
+ async () => {
8484
+ try {
8485
+ const response = await executeBash({
8486
+ command,
8487
+ ...headerTransforms ? { headerTransforms } : {},
8488
+ ...env ? { env } : {}
8489
+ });
8490
+ setSpanAttributes({
8491
+ "process.exit.code": response.exitCode,
8492
+ "app.sandbox.stdout_bytes": Buffer.byteLength(
8493
+ response.stdout ?? "",
8494
+ "utf8"
8495
+ ),
8496
+ "app.sandbox.stderr_bytes": Buffer.byteLength(
8497
+ response.stderr ?? "",
8498
+ "utf8"
8499
+ ),
8500
+ ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
8501
+ });
8502
+ setSpanStatus(response.exitCode === 0 ? "ok" : "error");
8503
+ return response;
8504
+ } catch (error) {
8505
+ setSpanAttributes({
8506
+ "error.type": error instanceof Error ? error.name : "sandbox_execute_error"
8507
+ });
8508
+ setSpanStatus("error");
8509
+ throw error;
8510
+ }
8511
+ }
8512
+ );
8513
+ return {
8514
+ result: {
8515
+ ok: result.exitCode === 0,
8516
+ command,
8517
+ cwd: SANDBOX_WORKSPACE_ROOT,
8518
+ exit_code: result.exitCode,
8519
+ signal: null,
8520
+ timed_out: false,
8521
+ stdout: result.stdout,
8522
+ stderr: result.stderr,
8523
+ stdout_truncated: result.stdoutTruncated,
8524
+ stderr_truncated: result.stderrTruncated
8525
+ }
8526
+ };
8527
+ };
8528
+ const executeReadFileTool = async (rawInput) => {
8529
+ const filePath = String(rawInput.path ?? "").trim();
8530
+ if (!filePath) {
8531
+ throw new Error("path is required");
8532
+ }
8533
+ if (!sessionManager.getSandboxId()) {
8534
+ const hostPath = resolveHostSkillPath(availableSkills, filePath) ?? resolveHostDataPath(referenceFiles, filePath);
8535
+ if (hostPath) {
8536
+ try {
8537
+ const content = await fs4.readFile(hostPath, "utf8");
8256
8538
  setSpanAttributes({
8539
+ "app.sandbox.path.length": filePath.length,
8257
8540
  "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8258
- "app.sandbox.read.chars": content.length
8541
+ "app.sandbox.read.chars": content.length,
8542
+ "app.skill.virtual_read": true
8259
8543
  });
8260
8544
  setSpanStatus("ok");
8261
8545
  return {
8262
- content,
8263
- path: filePath,
8264
- success: true
8546
+ result: {
8547
+ content,
8548
+ path: filePath,
8549
+ success: true
8550
+ }
8265
8551
  };
8266
- }
8267
- );
8268
- return { result };
8269
- }
8270
- 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);
8552
+ } catch (error) {
8553
+ if (!isHostFileMissingError(error)) {
8554
+ throw error;
8290
8555
  }
8291
8556
  }
8292
- );
8293
- return {
8294
- result: {
8295
- ok: true,
8557
+ }
8558
+ }
8559
+ logSandboxBootRequest("tool.readFile", {
8560
+ "file.path": filePath
8561
+ });
8562
+ const executeReadFile = (await sessionManager.ensureToolExecutors()).readFile;
8563
+ const result = await withSandboxSpan(
8564
+ "sandbox.readFile",
8565
+ "sandbox.fs.read",
8566
+ {
8567
+ "app.sandbox.path.length": filePath.length
8568
+ },
8569
+ async () => {
8570
+ const response = await executeReadFile({ path: filePath });
8571
+ const content = String(response.content ?? "");
8572
+ setSpanAttributes({
8573
+ "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8574
+ "app.sandbox.read.chars": content.length
8575
+ });
8576
+ setSpanStatus("ok");
8577
+ return {
8578
+ content,
8296
8579
  path: filePath,
8297
- bytes_written: Buffer.byteLength(content, "utf8")
8298
- }
8299
- };
8300
- }
8301
- throw new Error(`unsupported sandbox tool: ${params.toolName}`);
8580
+ success: true
8581
+ };
8582
+ }
8583
+ );
8584
+ return { result };
8302
8585
  };
8303
- const dispose = async () => {
8304
- if (!sandbox) {
8305
- return;
8306
- }
8586
+ const executeWriteFileTool = async (rawInput) => {
8587
+ const filePath = String(rawInput.path ?? "").trim();
8588
+ if (!filePath) {
8589
+ throw new Error("path is required");
8590
+ }
8591
+ const content = String(rawInput.content ?? "");
8592
+ logSandboxBootRequest("tool.writeFile", {
8593
+ "file.path": filePath
8594
+ });
8595
+ const executeWriteFile = (await sessionManager.ensureToolExecutors()).writeFile;
8307
8596
  await withSandboxSpan(
8308
- "sandbox.stop",
8309
- "sandbox.stop",
8597
+ "sandbox.writeFile",
8598
+ "sandbox.fs.write",
8310
8599
  {
8311
- "app.sandbox.stop.blocking": true
8600
+ "app.sandbox.path.length": filePath.length,
8601
+ "app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
8312
8602
  },
8313
8603
  async () => {
8314
- await sandbox.stop({ blocking: true });
8604
+ try {
8605
+ await executeWriteFile({ path: filePath, content });
8606
+ } catch (error) {
8607
+ throwSandboxOperationError("sandbox writeFile", error);
8608
+ }
8609
+ setSpanStatus("ok");
8315
8610
  }
8316
8611
  );
8317
- sandbox = null;
8318
- toolExecutors = void 0;
8612
+ return {
8613
+ result: {
8614
+ ok: true,
8615
+ path: filePath,
8616
+ bytes_written: Buffer.byteLength(content, "utf8")
8617
+ }
8618
+ };
8619
+ };
8620
+ const execute = async (params) => {
8621
+ const rawInput = params.input ?? {};
8622
+ const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
8623
+ if (params.toolName === "bash") {
8624
+ if (!bashCommand) {
8625
+ throw new Error("command is required");
8626
+ }
8627
+ if (options?.runBashCustomCommand) {
8628
+ const custom = await options.runBashCustomCommand(bashCommand);
8629
+ if (custom.handled) {
8630
+ return { result: custom.result };
8631
+ }
8632
+ }
8633
+ return await executeBashTool(rawInput, bashCommand);
8634
+ }
8635
+ if (params.toolName === "readFile") {
8636
+ return await executeReadFileTool(rawInput);
8637
+ }
8638
+ if (params.toolName === "writeFile") {
8639
+ return await executeWriteFileTool(rawInput);
8640
+ }
8641
+ throw new Error(`unsupported sandbox tool: ${params.toolName}`);
8319
8642
  };
8320
8643
  return {
8321
8644
  configureSkills(skills) {
8322
8645
  availableSkills = [...skills];
8646
+ sessionManager.configureSkills(skills);
8647
+ },
8648
+ configureReferenceFiles(files) {
8649
+ referenceFiles = [...files];
8650
+ sessionManager.configureReferenceFiles(files);
8323
8651
  },
8324
8652
  getSandboxId() {
8325
- return sandbox?.sandboxId ?? sandboxIdHint;
8653
+ return sessionManager.getSandboxId();
8326
8654
  },
8327
8655
  getDependencyProfileHash() {
8328
- return dependencyProfileHash;
8656
+ return sessionManager.getDependencyProfileHash();
8329
8657
  },
8330
8658
  canExecute(toolName) {
8331
8659
  return SANDBOX_TOOL_NAMES.has(toolName);
8332
8660
  },
8333
8661
  async createSandbox() {
8334
- return await acquireSandbox();
8662
+ return createSandboxWorkspace(await sessionManager.createSandbox());
8335
8663
  },
8336
8664
  execute,
8337
- dispose
8665
+ async dispose() {
8666
+ await sessionManager.dispose();
8667
+ }
8338
8668
  };
8339
8669
  }
8340
8670
 
@@ -8347,7 +8677,7 @@ function shouldEmitDevAgentTrace() {
8347
8677
  function buildToolStatus(toolName, input) {
8348
8678
  const obj = input && typeof input === "object" ? input : void 0;
8349
8679
  const command = obj ? compactStatusCommand(obj.command) : void 0;
8350
- const path6 = obj ? compactStatusPath(obj.path) : void 0;
8680
+ const path7 = obj ? compactStatusPath(obj.path) : void 0;
8351
8681
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
8352
8682
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
8353
8683
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
@@ -8362,8 +8692,8 @@ function buildToolStatus(toolName, input) {
8362
8692
  if (filename && toolName === "writeFile") {
8363
8693
  return makeAssistantStatus("updating", filename);
8364
8694
  }
8365
- if (path6 && toolName === "writeFile") {
8366
- return makeAssistantStatus("updating", path6);
8695
+ if (path7 && toolName === "writeFile") {
8696
+ return makeAssistantStatus("updating", path7);
8367
8697
  }
8368
8698
  if (skillName && toolName === "loadSkill") {
8369
8699
  return makeAssistantStatus("loading", skillName);
@@ -9156,6 +9486,14 @@ async function generateAssistantReply(messageText, context = {}) {
9156
9486
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
9157
9487
  let loadedSkillNamesForResume = [];
9158
9488
  let mcpToolManager;
9489
+ let sandboxExecutor;
9490
+ const getSandboxMetadata = () => sandboxExecutor ? {
9491
+ sandboxId: sandboxExecutor.getSandboxId(),
9492
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
9493
+ } : {
9494
+ sandboxId: lastKnownSandboxId,
9495
+ sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash
9496
+ };
9159
9497
  try {
9160
9498
  const shouldTrace = shouldEmitDevAgentTrace();
9161
9499
  const spanContext = {
@@ -9228,7 +9566,7 @@ async function generateAssistantReply(messageText, context = {}) {
9228
9566
  resolveConfiguration: async (key) => configurationValues[key]
9229
9567
  });
9230
9568
  const providerAuthActions = /* @__PURE__ */ new Map();
9231
- const sandboxExecutor = createSandboxExecutor({
9569
+ sandboxExecutor = createSandboxExecutor({
9232
9570
  sandboxId: context.sandbox?.sandboxId,
9233
9571
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
9234
9572
  traceContext: spanContext,
@@ -9255,10 +9593,53 @@ async function generateAssistantReply(messageText, context = {}) {
9255
9593
  return result.handled ? { handled: true, result: result.result } : { handled: false };
9256
9594
  }
9257
9595
  });
9258
- lastKnownSandboxId = sandboxExecutor.getSandboxId();
9259
- lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
9596
+ const currentSandboxExecutor = sandboxExecutor;
9260
9597
  sandboxExecutor.configureSkills(availableSkills);
9261
- const sandbox = await sandboxExecutor.createSandbox();
9598
+ sandboxExecutor.configureReferenceFiles(listReferenceFiles());
9599
+ let sandboxPromise;
9600
+ let sandboxPromiseId;
9601
+ const clearSandboxPromise = () => {
9602
+ sandboxPromise = void 0;
9603
+ sandboxPromiseId = void 0;
9604
+ };
9605
+ const getSandbox = (reason) => {
9606
+ const currentSandboxId = currentSandboxExecutor.getSandboxId();
9607
+ if (sandboxPromise && sandboxPromiseId && currentSandboxId !== sandboxPromiseId) {
9608
+ clearSandboxPromise();
9609
+ }
9610
+ if (!sandboxPromise) {
9611
+ logInfo(
9612
+ "sandbox_boot_requested",
9613
+ spanContext,
9614
+ {
9615
+ "app.sandbox.boot.trigger": reason.trigger,
9616
+ ...reason.path ? { "file.path": reason.path } : {},
9617
+ ...reason.cmd ? { "process.executable.name": reason.cmd } : {},
9618
+ ...reason.cwd ? { "file.directory": reason.cwd } : {}
9619
+ },
9620
+ "Lazy sandbox boot requested"
9621
+ );
9622
+ sandboxPromise = currentSandboxExecutor.createSandbox().then((sandbox2) => {
9623
+ sandboxPromiseId = sandbox2.sandboxId;
9624
+ return sandbox2;
9625
+ }).catch((error) => {
9626
+ clearSandboxPromise();
9627
+ throw error;
9628
+ });
9629
+ }
9630
+ return sandboxPromise;
9631
+ };
9632
+ const sandbox = {
9633
+ readFileToBuffer: async (input) => (await getSandbox({
9634
+ trigger: "workspace.readFileToBuffer",
9635
+ path: input.path
9636
+ })).readFileToBuffer(input),
9637
+ runCommand: async (input) => (await getSandbox({
9638
+ trigger: "workspace.runCommand",
9639
+ cmd: input.cmd,
9640
+ cwd: input.cwd
9641
+ })).runCommand(input)
9642
+ };
9262
9643
  for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
9263
9644
  const preloaded = await skillSandbox.loadSkill(skillName);
9264
9645
  if (preloaded) {
@@ -9393,20 +9774,37 @@ async function generateAssistantReply(messageText, context = {}) {
9393
9774
  activeSkills,
9394
9775
  invokedSkill
9395
9776
  ),
9396
- runtimeMetadata: getRuntimeMetadata()
9777
+ runtimeMetadata: getRuntimeMetadata(),
9778
+ threadParticipants: context.threadParticipants
9397
9779
  });
9398
9780
  const userContentParts = [{ type: "text", text: userTurnText }];
9399
9781
  for (const attachment of context.userAttachments ?? []) {
9400
- if (attachment.mediaType.startsWith("image/")) {
9782
+ if (attachment.promptText) {
9783
+ userContentParts.push({
9784
+ type: "text",
9785
+ text: attachment.promptText
9786
+ });
9787
+ } else if (attachment.mediaType.startsWith("image/")) {
9788
+ if (!attachment.data) {
9789
+ throw new Error("Image attachment is missing image data");
9790
+ }
9401
9791
  userContentParts.push({
9402
9792
  type: "image",
9403
9793
  data: attachment.data.toString("base64"),
9404
9794
  mimeType: attachment.mediaType
9405
9795
  });
9406
9796
  } else {
9797
+ if (!attachment.data) {
9798
+ throw new Error("Attachment is missing attachment data");
9799
+ }
9800
+ const promptAttachment = {
9801
+ data: attachment.data,
9802
+ mediaType: attachment.mediaType,
9803
+ filename: attachment.filename
9804
+ };
9407
9805
  userContentParts.push({
9408
9806
  type: "text",
9409
- text: encodeNonImageAttachmentForPrompt(attachment)
9807
+ text: encodeNonImageAttachmentForPrompt(promptAttachment)
9410
9808
  });
9411
9809
  }
9412
9810
  }
@@ -9603,8 +10001,8 @@ async function generateAssistantReply(messageText, context = {}) {
9603
10001
  replyFiles,
9604
10002
  artifactStatePatch,
9605
10003
  toolCalls,
9606
- sandboxId: sandboxExecutor.getSandboxId(),
9607
- sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
10004
+ sandboxId: currentSandboxExecutor.getSandboxId(),
10005
+ sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
9608
10006
  generatedFileCount: generatedFiles.length,
9609
10007
  hasTextDeltaCallback: Boolean(context.onTextDelta),
9610
10008
  shouldTrace,
@@ -9655,8 +10053,7 @@ async function generateAssistantReply(messageText, context = {}) {
9655
10053
  const message = error instanceof Error ? error.message : String(error);
9656
10054
  return {
9657
10055
  text: `Error: ${message}`,
9658
- sandboxId: lastKnownSandboxId,
9659
- sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
10056
+ ...getSandboxMetadata(),
9660
10057
  diagnostics: {
9661
10058
  outcome: "provider_error",
9662
10059
  modelId: botConfig.modelId,
@@ -10260,9 +10657,9 @@ async function GET4(request, provider, waitUntil) {
10260
10657
  }
10261
10658
 
10262
10659
  // src/chat/slack/app-home.ts
10263
- import fs4 from "fs";
10264
- import path5 from "path";
10265
- var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
10660
+ import fs5 from "fs";
10661
+ import path6 from "path";
10662
+ var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
10266
10663
  var MAX_HOME_SKILLS = 6;
10267
10664
  var MAX_SECTION_TEXT_CHARS = 3e3;
10268
10665
  var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
@@ -10272,16 +10669,16 @@ function clampSectionText(text) {
10272
10669
  }
10273
10670
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
10274
10671
  }
10275
- function loadAboutText() {
10276
- const aboutPath = path5.join(homeDir(), "ABOUT.md");
10672
+ function loadDescriptionText() {
10673
+ const descriptionPath = path6.join(homeDir(), "DESCRIPTION.md");
10277
10674
  try {
10278
- const raw = fs4.readFileSync(aboutPath, "utf8").trim();
10675
+ const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
10279
10676
  if (raw.length > 0) {
10280
10677
  return clampSectionText(raw);
10281
10678
  }
10282
10679
  } catch {
10283
10680
  }
10284
- return DEFAULT_ABOUT_TEXT;
10681
+ return DEFAULT_DESCRIPTION_TEXT;
10285
10682
  }
10286
10683
  async function buildSkillsSummaryText() {
10287
10684
  const skills = (await discoverSkills()).filter(
@@ -10301,7 +10698,10 @@ async function buildSkillsSummaryText() {
10301
10698
  }
10302
10699
  async function hasConnectedAccount(userId, plugin, userTokenStore) {
10303
10700
  if (plugin.manifest.credentials?.type === "oauth-bearer") {
10304
- return Boolean(await userTokenStore.get(userId, plugin.manifest.name));
10701
+ const stored = await userTokenStore.get(userId, plugin.manifest.name);
10702
+ return Boolean(
10703
+ stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
10704
+ );
10305
10705
  }
10306
10706
  if (plugin.manifest.mcp) {
10307
10707
  return Boolean(
@@ -10312,7 +10712,7 @@ async function hasConnectedAccount(userId, plugin, userTokenStore) {
10312
10712
  }
10313
10713
  async function buildHomeView(userId, userTokenStore) {
10314
10714
  const runtimeMetadata = getRuntimeMetadata();
10315
- const aboutText = loadAboutText();
10715
+ const descriptionText = loadDescriptionText();
10316
10716
  const skillsSummaryText = await buildSkillsSummaryText();
10317
10717
  const providers = getPluginProviders();
10318
10718
  const connectedSections = [];
@@ -10357,7 +10757,7 @@ ${plugin.manifest.description}`
10357
10757
  type: "section",
10358
10758
  text: {
10359
10759
  type: "mrkdwn",
10360
- text: aboutText
10760
+ text: descriptionText
10361
10761
  }
10362
10762
  },
10363
10763
  { type: "divider" },
@@ -10564,7 +10964,10 @@ async function GET5(request, provider, waitUntil) {
10564
10964
  const tokenData = await tokenResponse.json();
10565
10965
  let parsedTokenResponse;
10566
10966
  try {
10567
- parsedTokenResponse = parseOAuthTokenResponse(tokenData);
10967
+ parsedTokenResponse = parseOAuthTokenResponse(
10968
+ tokenData,
10969
+ providerConfig.scope
10970
+ );
10568
10971
  } catch {
10569
10972
  return htmlErrorResponse(
10570
10973
  "Connection failed",
@@ -10572,6 +10975,13 @@ async function GET5(request, provider, waitUntil) {
10572
10975
  500
10573
10976
  );
10574
10977
  }
10978
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, providerConfig.scope)) {
10979
+ return htmlErrorResponse(
10980
+ "Connection failed",
10981
+ `The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
10982
+ 400
10983
+ );
10984
+ }
10575
10985
  const userTokenStore = createUserTokenStore();
10576
10986
  await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
10577
10987
  waitUntil(async () => {
@@ -10625,7 +11035,7 @@ var replyDecisionSchema = z.object({
10625
11035
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
10626
11036
  var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
10627
11037
  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;
11038
+ var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
10629
11039
  var THREAD_OPTOUT_PATTERNS = [
10630
11040
  /\bstop (?:watching|replying|participating)\b/i,
10631
11041
  /\bstay out\b/i,
@@ -10633,6 +11043,11 @@ var THREAD_OPTOUT_PATTERNS = [
10633
11043
  /\bunsubscribe\b/i,
10634
11044
  /\bleave (?:this )?thread\b/i
10635
11045
  ];
11046
+ 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;
11047
+ 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;
11048
+ 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;
11049
+ var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
11050
+ var RECENT_THREAD_WINDOW = 6;
10636
11051
  function escapeRegExp(value) {
10637
11052
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10638
11053
  }
@@ -10669,36 +11084,113 @@ function isThreadOptOutInstruction(rawText, text) {
10669
11084
  (pattern) => pattern.test(rawText) || pattern.test(text)
10670
11085
  );
10671
11086
  }
10672
- function getTranscriptMessageHints(conversationContext) {
11087
+ function isAcknowledgmentOnly(text) {
11088
+ return ACKNOWLEDGMENT_ONLY_RE.test(text.trim());
11089
+ }
11090
+ function hasDirectedFollowUpCue(text) {
11091
+ return DIRECTED_FOLLOW_UP_CUE_RE.test(text.trim());
11092
+ }
11093
+ function isTerseClarification(text) {
11094
+ return TERSE_CLARIFICATION_RE.test(text.trim());
11095
+ }
11096
+ function isGenericImmediateSideConversation(text) {
11097
+ const trimmed = text.trim();
11098
+ if (GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE.test(trimmed)) {
11099
+ return true;
11100
+ }
11101
+ if (!trimmed.toLowerCase().startsWith("what about")) {
11102
+ return false;
11103
+ }
11104
+ const wordCount = trimmed.split(/\s+/).map((part) => part.trim()).filter(Boolean).length;
11105
+ return wordCount > 3;
11106
+ }
11107
+ function parseTranscriptMessages(conversationContext) {
10673
11108
  if (!conversationContext) {
10674
- return {
10675
- latestPriorMessageRole: "[none]",
10676
- latestPriorAssistantMessage: "[none]"
10677
- };
11109
+ return [];
10678
11110
  }
11111
+ const messages = [];
10679
11112
  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);
11113
+ for (const line of lines) {
11114
+ const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
10684
11115
  if (!match) {
10685
11116
  continue;
10686
11117
  }
10687
- if (latestPriorMessageRole === "[none]") {
10688
- latestPriorMessageRole = match[1].toLowerCase();
10689
- }
10690
- if (latestPriorAssistantMessage === "[none]" && match[1].toLowerCase() === "assistant") {
10691
- latestPriorAssistantMessage = match[2];
11118
+ messages.push({
11119
+ role: match[1].toLowerCase(),
11120
+ author: match[2]?.trim() || "unknown",
11121
+ text: match[3]?.trim() || ""
11122
+ });
11123
+ }
11124
+ return messages;
11125
+ }
11126
+ function buildRouterSignals(input) {
11127
+ const transcriptMessages = parseTranscriptMessages(input.conversationContext);
11128
+ const recentMessages = transcriptMessages.filter((message) => message.role !== "system").slice(-RECENT_THREAD_WINDOW);
11129
+ const latestPriorMessage = [...transcriptMessages].reverse().find((message) => message.role !== "system");
11130
+ const latestPriorAssistantMessage = [...transcriptMessages].reverse().find((message) => message.role === "assistant");
11131
+ let humanMessagesSinceLastAssistant;
11132
+ let humanMessageCount = 0;
11133
+ for (let index = transcriptMessages.length - 1; index >= 0; index -= 1) {
11134
+ const message = transcriptMessages[index];
11135
+ if (!message || message.role === "system") {
11136
+ continue;
10692
11137
  }
10693
- if (latestPriorMessageRole !== "[none]" && latestPriorAssistantMessage !== "[none]") {
11138
+ if (message.role === "assistant") {
11139
+ humanMessagesSinceLastAssistant = humanMessageCount;
10694
11140
  break;
10695
11141
  }
11142
+ humanMessageCount += 1;
10696
11143
  }
10697
11144
  return {
10698
- latestPriorMessageRole,
10699
- latestPriorAssistantMessage
11145
+ assistantWasLastSpeaker: latestPriorMessage?.role === "assistant",
11146
+ currentMessageHasDirectedFollowUpCue: hasDirectedFollowUpCue(input.text),
11147
+ currentMessageHasAttachments: Boolean(input.hasAttachments),
11148
+ currentMessageIsTerseClarification: isTerseClarification(input.text),
11149
+ humanMessagesSinceLastAssistant,
11150
+ latestPriorAssistantMessage: latestPriorAssistantMessage?.text || "[none]",
11151
+ latestPriorMessageRole: latestPriorMessage?.role || "[none]",
11152
+ recentMessages
10700
11153
  };
10701
11154
  }
11155
+ function buildRouterPrompt(rawText, signals) {
11156
+ const recentThread = signals.recentMessages.length > 0 ? signals.recentMessages.map(
11157
+ (message) => escapeXml(`[${message.role}] ${message.author}: ${message.text}`)
11158
+ ).join("\n") : "[none]";
11159
+ return [
11160
+ `<latest-message>${escapeXml(rawText.trim() || "[attachment-only message]")}</latest-message>`,
11161
+ "<routing-signals>",
11162
+ `assistant_was_last_speaker=${signals.assistantWasLastSpeaker ? "true" : "false"}`,
11163
+ `human_messages_since_last_assistant=${signals.humanMessagesSinceLastAssistant ?? "none"}`,
11164
+ `latest_prior_message_role=${escapeXml(signals.latestPriorMessageRole)}`,
11165
+ `current_message_has_directed_follow_up_cue=${signals.currentMessageHasDirectedFollowUpCue ? "true" : "false"}`,
11166
+ `current_message_is_terse_clarification=${signals.currentMessageIsTerseClarification ? "true" : "false"}`,
11167
+ `current_message_has_attachments=${signals.currentMessageHasAttachments ? "true" : "false"}`,
11168
+ "</routing-signals>",
11169
+ `<latest-prior-assistant-message>${escapeXml(
11170
+ signals.latestPriorAssistantMessage
11171
+ )}</latest-prior-assistant-message>`,
11172
+ "<recent-thread>",
11173
+ recentThread,
11174
+ "</recent-thread>"
11175
+ ].join("\n");
11176
+ }
11177
+ function getReplyConfidenceThreshold(signals) {
11178
+ let threshold = ROUTER_CONFIDENCE_THRESHOLD;
11179
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0) {
11180
+ if (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification) {
11181
+ threshold = 0.65;
11182
+ } else {
11183
+ threshold = 0.9;
11184
+ }
11185
+ } else if (signals.humanMessagesSinceLastAssistant === 1) {
11186
+ threshold = signals.currentMessageHasDirectedFollowUpCue ? 0.8 : 0.9;
11187
+ } else if (signals.humanMessagesSinceLastAssistant === void 0) {
11188
+ threshold = 0.85;
11189
+ } else if (signals.humanMessagesSinceLastAssistant >= 2) {
11190
+ threshold = 0.9;
11191
+ }
11192
+ return Math.max(0.6, Math.min(0.9, threshold));
11193
+ }
10702
11194
  function getSubscribedReplyPreflightDecision(args) {
10703
11195
  const text = args.text.trim();
10704
11196
  const rawText = args.rawText.trim();
@@ -10719,54 +11211,27 @@ function getSubscribedReplyPreflightDecision(args) {
10719
11211
  reasonDetail: leadingOtherPartyAddress
10720
11212
  };
10721
11213
  }
10722
- function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
10723
- const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
11214
+ function buildRouterSystemPrompt(botUserName) {
10724
11215
  return [
10725
11216
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
10726
11217
  "Decide whether Junior should reply to the latest message.",
10727
11218
  "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.",
11219
+ "Reply true only when the latest message is aimed at Junior.",
11220
+ "Use who currently has the conversation floor, not just topic overlap.",
11221
+ "If Junior was the last speaker, only a clear turn back to Junior should count as an implicit follow-up.",
11222
+ "Terse clarifications like 'which one?' or 'why?' right after Junior answers can be should_reply=true.",
11223
+ "Direct self-reference to Junior's prior answer like 'what did you just say?' or 'explain that more' can be should_reply=true.",
11224
+ "If one or more humans spoke after Junior, require a clear turn back to Junior. Shared domain vocabulary alone is not enough.",
11225
+ "Questions like 'what about auth?' or 'can you check on this?' are usually human-to-human unless the thread clearly turns back to Junior.",
11226
+ "A vague question like 'is that the right approach?' is still should_reply=false unless it clearly turns back to Junior.",
11227
+ "Acknowledgments, reactions, status chatter, and team coordination should be should_reply=false.",
11228
+ "If the latest message clearly tells Junior to stop watching, replying, or participating, set should_unsubscribe=true and should_reply=false.",
11229
+ "When uncertain, prefer should_reply=false with low confidence.",
10761
11230
  "",
10762
11231
  "Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
10763
11232
  "Do not return any extra keys.",
10764
11233
  "",
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>`
11234
+ `<assistant-name>${escapeXml(botUserName)}</assistant-name>`
10770
11235
  ].join("\n");
10771
11236
  }
10772
11237
  async function decideSubscribedThreadReply(args) {
@@ -10781,11 +11246,16 @@ async function decideSubscribedThreadReply(args) {
10781
11246
  if (preflightDecision) {
10782
11247
  return preflightDecision;
10783
11248
  }
11249
+ const signals = buildRouterSignals(args.input);
10784
11250
  if (!text && !args.input.hasAttachments) {
10785
11251
  return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
10786
11252
  }
10787
- if (!text && args.input.hasAttachments) {
10788
- return { shouldReply: true, reason: "attachment_only" /* AttachmentOnly */ };
11253
+ if (!args.input.isExplicitMention && !args.input.hasAttachments && isAcknowledgmentOnly(text)) {
11254
+ return {
11255
+ shouldReply: false,
11256
+ reason: "side_conversation" /* SideConversation */,
11257
+ reasonDetail: "acknowledgment"
11258
+ };
10789
11259
  }
10790
11260
  if (args.input.isExplicitMention) {
10791
11261
  if (isThreadOptOutInstruction(rawText, text)) {
@@ -10801,18 +11271,21 @@ async function decideSubscribedThreadReply(args) {
10801
11271
  reason: "explicit_mention" /* ExplicitMention */
10802
11272
  };
10803
11273
  }
11274
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
11275
+ return {
11276
+ shouldReply: false,
11277
+ reason: "side_conversation" /* SideConversation */,
11278
+ reasonDetail: "generic immediate side conversation"
11279
+ };
11280
+ }
10804
11281
  try {
10805
11282
  const result = await args.completeObject({
10806
11283
  modelId: args.modelId,
10807
11284
  schema: replyDecisionSchema,
10808
11285
  maxTokens: 120,
10809
11286
  temperature: 0,
10810
- system: buildRouterSystemPrompt(
10811
- args.botUserName,
10812
- args.input.conversationContext,
10813
- args.input.isExplicitMention
10814
- ),
10815
- prompt: rawText,
11287
+ system: buildRouterSystemPrompt(args.botUserName),
11288
+ prompt: buildRouterPrompt(rawText, signals),
10816
11289
  metadata: {
10817
11290
  modelId: args.modelId,
10818
11291
  threadId: args.input.context.threadId ?? "",
@@ -10823,6 +11296,7 @@ async function decideSubscribedThreadReply(args) {
10823
11296
  });
10824
11297
  const parsed = replyDecisionSchema.parse(result.object);
10825
11298
  const reason = parsed.reason?.trim() || "classifier";
11299
+ const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
10826
11300
  if (parsed.should_unsubscribe) {
10827
11301
  if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
10828
11302
  return {
@@ -10845,7 +11319,7 @@ async function decideSubscribedThreadReply(args) {
10845
11319
  reasonDetail: reason
10846
11320
  };
10847
11321
  }
10848
- if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
11322
+ if (parsed.confidence < replyConfidenceThreshold) {
10849
11323
  return {
10850
11324
  shouldReply: false,
10851
11325
  reason: "low_confidence" /* LowConfidence */,
@@ -11440,16 +11914,166 @@ var MAX_USER_ATTACHMENTS = 3;
11440
11914
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
11441
11915
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
11442
11916
  var MAX_VISION_SUMMARY_CHARS = 500;
11443
- async function resolveUserAttachments(attachments, context) {
11917
+ function isVisionEnabled() {
11918
+ return Boolean(botConfig.visionModelId);
11919
+ }
11920
+ var ImageAttachmentProcessingError = class extends Error {
11921
+ constructor(message) {
11922
+ super(message);
11923
+ this.name = "ImageAttachmentProcessingError";
11924
+ }
11925
+ };
11926
+ function buildImageAttachmentPromptText(args) {
11927
+ return [
11928
+ "<image-attachment>",
11929
+ `filename: ${args.filename ?? "unnamed"}`,
11930
+ `media_type: ${args.mediaType}`,
11931
+ "<summary>",
11932
+ args.summary,
11933
+ "</summary>",
11934
+ "</image-attachment>"
11935
+ ].join("\n");
11936
+ }
11937
+ async function summarizeImageWithVision(args) {
11938
+ const visionModelId = botConfig.visionModelId;
11939
+ if (!visionModelId) {
11940
+ return void 0;
11941
+ }
11942
+ const result = await args.completeText({
11943
+ modelId: visionModelId,
11944
+ temperature: 0,
11945
+ maxTokens: args.maxTokens,
11946
+ messages: [
11947
+ {
11948
+ role: "user",
11949
+ content: [
11950
+ {
11951
+ type: "text",
11952
+ text: args.prompt
11953
+ },
11954
+ {
11955
+ type: "image",
11956
+ data: args.imageData.toString("base64"),
11957
+ mimeType: args.mimeType
11958
+ }
11959
+ ],
11960
+ timestamp: Date.now()
11961
+ }
11962
+ ],
11963
+ metadata: {
11964
+ modelId: visionModelId,
11965
+ ...args.metadata
11966
+ }
11967
+ });
11968
+ const summary = result.text.trim().replace(/\s+/g, " ");
11969
+ return summary || void 0;
11970
+ }
11971
+ function truncateVisionSummary(summary) {
11972
+ return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
11973
+ }
11974
+ function getCachedImageSummaries(args) {
11975
+ if (!args.conversation || !args.messageTs) {
11976
+ return [];
11977
+ }
11978
+ const conversationMessage = args.conversation.messages.find(
11979
+ (message) => getConversationMessageSlackTs(message) === args.messageTs
11980
+ );
11981
+ if (!conversationMessage) {
11982
+ return [];
11983
+ }
11984
+ return (conversationMessage.meta?.imageFileIds ?? []).map(
11985
+ (fileId) => args.conversation?.vision.byFileId[fileId]?.summary?.trim()
11986
+ );
11987
+ }
11988
+ function createImageAttachmentProcessingError(attachment) {
11989
+ const label = attachment.filename ? `"${attachment.filename}"` : "this image";
11990
+ return new ImageAttachmentProcessingError(
11991
+ `Image attachment ${label} could not be analyzed`
11992
+ );
11993
+ }
11994
+ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
11444
11995
  if (!attachments || attachments.length === 0) {
11445
11996
  return [];
11446
11997
  }
11447
11998
  const results = [];
11999
+ const cachedImageSummaries = getCachedImageSummaries({
12000
+ conversation: context.conversation,
12001
+ messageTs: context.messageTs
12002
+ });
12003
+ let nextCachedImageSummaryIndex = 0;
11448
12004
  for (const attachment of attachments) {
11449
12005
  if (results.length >= MAX_USER_ATTACHMENTS) break;
11450
12006
  if (attachment.type !== "image" && attachment.type !== "file") continue;
11451
12007
  const mediaType = attachment.mimeType ?? "application/octet-stream";
12008
+ const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
12009
+ if (isImageAttachment && !isVisionEnabled()) {
12010
+ continue;
12011
+ }
11452
12012
  try {
12013
+ const resolvedAttachment = {
12014
+ mediaType,
12015
+ filename: attachment.name
12016
+ };
12017
+ if (isImageAttachment) {
12018
+ const cachedSummary = cachedImageSummaries[nextCachedImageSummaryIndex];
12019
+ nextCachedImageSummaryIndex += 1;
12020
+ if (cachedSummary) {
12021
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
12022
+ filename: attachment.name,
12023
+ mediaType,
12024
+ summary: cachedSummary
12025
+ });
12026
+ results.push(resolvedAttachment);
12027
+ continue;
12028
+ }
12029
+ let imageData = null;
12030
+ if (attachment.fetchData) {
12031
+ imageData = await attachment.fetchData();
12032
+ } else if (attachment.data instanceof Buffer) {
12033
+ imageData = attachment.data;
12034
+ }
12035
+ if (!imageData) {
12036
+ throw createImageAttachmentProcessingError({
12037
+ filename: attachment.name
12038
+ });
12039
+ }
12040
+ if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
12041
+ throw createImageAttachmentProcessingError({
12042
+ filename: attachment.name
12043
+ });
12044
+ }
12045
+ const summary = await summarizeImageWithVision({
12046
+ completeText: deps.completeText,
12047
+ imageData,
12048
+ mimeType: mediaType,
12049
+ maxTokens: 220,
12050
+ prompt: [
12051
+ "Extract concise, factual context from this user-provided image.",
12052
+ "Focus on visible text, UI state, charts, diagrams, errors, names, and other concrete details useful for answering the user's current request.",
12053
+ "Do not speculate.",
12054
+ "Return plain text only."
12055
+ ].join(" "),
12056
+ metadata: {
12057
+ threadId: context.threadId ?? "",
12058
+ channelId: context.channelId ?? "",
12059
+ requesterId: context.requesterId ?? "",
12060
+ runId: context.runId ?? "",
12061
+ filename: attachment.name ?? ""
12062
+ }
12063
+ });
12064
+ if (!summary) {
12065
+ throw createImageAttachmentProcessingError({
12066
+ filename: attachment.name
12067
+ });
12068
+ }
12069
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
12070
+ filename: attachment.name,
12071
+ mediaType,
12072
+ summary: truncateVisionSummary(summary)
12073
+ });
12074
+ results.push(resolvedAttachment);
12075
+ continue;
12076
+ }
11453
12077
  let data = null;
11454
12078
  if (attachment.fetchData) {
11455
12079
  data = await attachment.fetchData();
@@ -11476,12 +12100,32 @@ async function resolveUserAttachments(attachments, context) {
11476
12100
  );
11477
12101
  continue;
11478
12102
  }
11479
- results.push({
11480
- data,
11481
- mediaType,
11482
- filename: attachment.name
11483
- });
12103
+ resolvedAttachment.data = data;
12104
+ results.push(resolvedAttachment);
11484
12105
  } catch (error) {
12106
+ if (isImageAttachment) {
12107
+ const attachmentError = error instanceof ImageAttachmentProcessingError ? error : createImageAttachmentProcessingError({
12108
+ filename: attachment.name
12109
+ });
12110
+ logWarn(
12111
+ "image_attachment_processing_failed",
12112
+ {
12113
+ slackThreadId: context.threadId,
12114
+ slackUserId: context.requesterId,
12115
+ slackChannelId: context.channelId,
12116
+ runId: context.runId,
12117
+ assistantUserName: botConfig.userName,
12118
+ modelId: botConfig.visionModelId ?? botConfig.modelId
12119
+ },
12120
+ {
12121
+ "error.message": error instanceof Error ? error.message : String(error),
12122
+ "file.mime_type": mediaType,
12123
+ ...attachment.name ? { "file.name": attachment.name } : {}
12124
+ },
12125
+ "Image attachment processing failed"
12126
+ );
12127
+ throw attachmentError;
12128
+ }
11485
12129
  logWarn(
11486
12130
  "attachment_resolution_failed",
11487
12131
  {
@@ -11503,35 +12147,23 @@ async function resolveUserAttachments(attachments, context) {
11503
12147
  return results;
11504
12148
  }
11505
12149
  async function summarizeConversationImage(args, deps) {
12150
+ const visionModelId = botConfig.visionModelId;
12151
+ if (!visionModelId) {
12152
+ return void 0;
12153
+ }
11506
12154
  try {
11507
- const result = await deps.completeText({
11508
- modelId: botConfig.modelId,
11509
- temperature: 0,
12155
+ const summary = await summarizeImageWithVision({
12156
+ completeText: deps.completeText,
12157
+ imageData: args.imageData,
12158
+ mimeType: args.mimeType,
11510
12159
  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
- ],
12160
+ prompt: [
12161
+ "Extract concise, factual context from this image for future thread turns.",
12162
+ "Focus on visible text, names, titles, companies, and candidate-identifying details.",
12163
+ "Do not speculate.",
12164
+ "Return plain text only."
12165
+ ].join(" "),
11533
12166
  metadata: {
11534
- modelId: botConfig.modelId,
11535
12167
  threadId: args.context.threadId ?? "",
11536
12168
  channelId: args.context.channelId ?? "",
11537
12169
  requesterId: args.context.requesterId ?? "",
@@ -11539,11 +12171,10 @@ async function summarizeConversationImage(args, deps) {
11539
12171
  fileId: args.fileId
11540
12172
  }
11541
12173
  });
11542
- const summary = result.text.trim().replace(/\s+/g, " ");
11543
12174
  if (!summary) {
11544
12175
  return void 0;
11545
12176
  }
11546
- return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
12177
+ return truncateVisionSummary(summary);
11547
12178
  } catch (error) {
11548
12179
  logWarn(
11549
12180
  "conversation_image_vision_failed",
@@ -11553,7 +12184,7 @@ async function summarizeConversationImage(args, deps) {
11553
12184
  slackChannelId: args.context.channelId,
11554
12185
  runId: args.context.runId,
11555
12186
  assistantUserName: botConfig.userName,
11556
- modelId: botConfig.modelId
12187
+ modelId: visionModelId
11557
12188
  },
11558
12189
  {
11559
12190
  "error.message": error instanceof Error ? error.message : String(error),
@@ -11566,6 +12197,9 @@ async function summarizeConversationImage(args, deps) {
11566
12197
  }
11567
12198
  }
11568
12199
  async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
12200
+ if (!isVisionEnabled()) {
12201
+ return;
12202
+ }
11569
12203
  if (!context.channelId || !context.threadTs) {
11570
12204
  return;
11571
12205
  }
@@ -11766,6 +12400,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
11766
12400
  }
11767
12401
  function createVisionContextService(deps) {
11768
12402
  return {
12403
+ resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
11769
12404
  hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
11770
12405
  conversation,
11771
12406
  context,
@@ -11859,6 +12494,19 @@ function getExecutionFailureReason(reply) {
11859
12494
  }
11860
12495
  return "empty assistant turn";
11861
12496
  }
12497
+ function buildParticipants(messages) {
12498
+ const seen = /* @__PURE__ */ new Set();
12499
+ const participants = [];
12500
+ for (const message of messages) {
12501
+ const { userId, userName, fullName } = message.author ?? {};
12502
+ if (!userId || message.author?.isBot) continue;
12503
+ if (!seen.has(userId)) {
12504
+ seen.add(userId);
12505
+ participants.push({ userId, userName, fullName });
12506
+ }
12507
+ }
12508
+ return participants;
12509
+ }
11862
12510
  function createReplyToThread(deps) {
11863
12511
  return async function replyToThread(thread, message, options = {}) {
11864
12512
  if (message.author.isMe) {
@@ -11901,6 +12549,7 @@ function createReplyToThread(deps) {
11901
12549
  runId
11902
12550
  }
11903
12551
  });
12552
+ const slackMessageTs = getSlackMessageTs(message);
11904
12553
  const turnId = buildDeterministicTurnId(message.id);
11905
12554
  startActiveTurn({
11906
12555
  conversation: preparedState.conversation,
@@ -11945,13 +12594,15 @@ function createReplyToThread(deps) {
11945
12594
  if (resolvedUserName) {
11946
12595
  setTags({ slackUserName: resolvedUserName });
11947
12596
  }
11948
- const userAttachments = await resolveUserAttachments(
12597
+ const userAttachments = await deps.resolveUserAttachments(
11949
12598
  message.attachments,
11950
12599
  {
11951
12600
  threadId,
11952
12601
  requesterId: message.author.userId,
11953
12602
  channelId,
11954
- runId
12603
+ runId,
12604
+ conversation: preparedState.conversation,
12605
+ messageTs: slackMessageTs
11955
12606
  }
11956
12607
  );
11957
12608
  const progress = createProgressReporter({
@@ -12015,6 +12666,9 @@ function createReplyToThread(deps) {
12015
12666
  let shouldPersistFailureState = true;
12016
12667
  try {
12017
12668
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
12669
+ const threadParticipants = buildParticipants(
12670
+ preparedState.conversation.messages
12671
+ );
12018
12672
  const reply = await deps.services.generateAssistantReply(userText, {
12019
12673
  assistant: {
12020
12674
  userName: botConfig.userName
@@ -12044,6 +12698,7 @@ function createReplyToThread(deps) {
12044
12698
  sandboxId: preparedState.sandboxId,
12045
12699
  sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
12046
12700
  },
12701
+ threadParticipants,
12047
12702
  onStatus: (status) => progress.setStatus(status),
12048
12703
  onTextDelta: (deltaText) => {
12049
12704
  if (explicitChannelPostIntent) {
@@ -12340,6 +12995,7 @@ function createPrepareTurnState(deps) {
12340
12995
  }
12341
12996
  );
12342
12997
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
12998
+ const slackTs = getSlackMessageTs(args.message);
12343
12999
  const incomingUserMessage = {
12344
13000
  id: args.message.id,
12345
13001
  role: "user",
@@ -12353,7 +13009,7 @@ function createPrepareTurnState(deps) {
12353
13009
  },
12354
13010
  meta: {
12355
13011
  explicitMention: args.explicitMention,
12356
- slackTs: args.message.id,
13012
+ slackTs,
12357
13013
  imagesHydrated: !messageHasPotentialImageAttachment
12358
13014
  }
12359
13015
  };
@@ -12361,7 +13017,7 @@ function createPrepareTurnState(deps) {
12361
13017
  conversation,
12362
13018
  incomingUserMessage
12363
13019
  );
12364
- if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
13020
+ if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
12365
13021
  await deps.hydrateConversationVisionContext(conversation, {
12366
13022
  threadId: args.context.threadId,
12367
13023
  channelId: args.context.channelId,
@@ -12408,6 +13064,7 @@ function createSlackRuntime(options) {
12408
13064
  const replyToThread = createReplyToThread({
12409
13065
  getSlackAdapter: options.getSlackAdapter,
12410
13066
  prepareTurnState,
13067
+ resolveUserAttachments: services.visionContext.resolveUserAttachments,
12411
13068
  services: services.replyExecutor
12412
13069
  });
12413
13070
  return createSlackTurnRuntime({
@@ -12439,6 +13096,7 @@ function createSlackRuntime(options) {
12439
13096
  }) => {
12440
13097
  const conversation = coerceThreadConversationState(await thread.state);
12441
13098
  const normalizedUserText = normalizeConversationText(userText) || "[non-text message]";
13099
+ const slackTs = getSlackMessageTs(message);
12442
13100
  upsertConversationMessage(conversation, {
12443
13101
  id: message.id,
12444
13102
  role: "user",
@@ -12452,7 +13110,7 @@ function createSlackRuntime(options) {
12452
13110
  },
12453
13111
  meta: {
12454
13112
  explicitMention: Boolean(message.isMention),
12455
- slackTs: message.id,
13113
+ slackTs,
12456
13114
  replied: false,
12457
13115
  skippedReason: decision.reason,
12458
13116
  imagesHydrated: true
@@ -12974,9 +13632,121 @@ function getProductionBot() {
12974
13632
  return productionBot;
12975
13633
  }
12976
13634
 
13635
+ // src/chat/ingress/message-changed.ts
13636
+ import { Message } from "chat";
13637
+ function getEditedMentionMessageId(messageTs) {
13638
+ return `${messageTs}:message_changed_mention`;
13639
+ }
13640
+ function isMessageChangedEnvelope(value) {
13641
+ if (!value || typeof value !== "object") return false;
13642
+ const v = value;
13643
+ if (v.type !== "event_callback") return false;
13644
+ const event = v.event;
13645
+ if (!event || typeof event !== "object") return false;
13646
+ return event.type === "message" && event.subtype === "message_changed" && typeof event.channel === "string" && typeof event.message === "object" && event.message !== null && typeof event.previous_message === "object" && event.previous_message !== null;
13647
+ }
13648
+ function textMentionsBot(text, botUserId) {
13649
+ return text.includes(`<@${botUserId}>`);
13650
+ }
13651
+ function extractMessageChangedMention(body, botUserId, adapter) {
13652
+ if (!isMessageChangedEnvelope(body)) return null;
13653
+ const { event } = body;
13654
+ const newText = event.message.text ?? "";
13655
+ const prevText = event.previous_message.text ?? "";
13656
+ if (!textMentionsBot(newText, botUserId)) return null;
13657
+ if (textMentionsBot(prevText, botUserId)) return null;
13658
+ const channelId = event.channel;
13659
+ const messageTs = event.message.ts;
13660
+ const threadTs = event.message.thread_ts ?? messageTs;
13661
+ const userId = event.message.user ?? "unknown";
13662
+ const threadId = `slack:${channelId}:${threadTs}`;
13663
+ const raw = {
13664
+ channel: channelId,
13665
+ ts: messageTs,
13666
+ thread_ts: threadTs,
13667
+ user: userId
13668
+ };
13669
+ const message = new Message({
13670
+ id: getEditedMentionMessageId(messageTs),
13671
+ threadId,
13672
+ text: newText,
13673
+ isMention: true,
13674
+ attachments: [],
13675
+ metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
13676
+ formatted: { type: "root", children: [] },
13677
+ raw,
13678
+ author: {
13679
+ userId,
13680
+ userName: userId,
13681
+ fullName: userId,
13682
+ isBot: false,
13683
+ isMe: false
13684
+ }
13685
+ });
13686
+ Object.defineProperty(message, "adapter", {
13687
+ configurable: true,
13688
+ enumerable: false,
13689
+ value: adapter,
13690
+ writable: true
13691
+ });
13692
+ return { threadId, message };
13693
+ }
13694
+
12977
13695
  // src/handlers/webhooks.ts
12978
- async function POST(request, platform, waitUntil) {
12979
- const bot = getProductionBot();
13696
+ function getSlackPayloadTeamId(body) {
13697
+ if (!body || typeof body !== "object") {
13698
+ return void 0;
13699
+ }
13700
+ const teamId = body.team_id;
13701
+ return typeof teamId === "string" && teamId.length > 0 ? teamId : void 0;
13702
+ }
13703
+ async function handleAuthenticatedSlackMessageChangedMention(args) {
13704
+ const slackAdapter = args.bot.getAdapter("slack");
13705
+ const authAdapter = slackAdapter;
13706
+ const timestamp = args.request.headers.get("x-slack-request-timestamp");
13707
+ const signature = args.request.headers.get("x-slack-signature");
13708
+ if (!authAdapter.verifySignature(args.rawBody, timestamp, signature)) {
13709
+ return;
13710
+ }
13711
+ const webhookOptions = {
13712
+ waitUntil: (task) => args.waitUntil(task)
13713
+ };
13714
+ const dispatch = () => {
13715
+ const botUserId = authAdapter.botUserId;
13716
+ if (!botUserId) {
13717
+ return false;
13718
+ }
13719
+ const result = extractMessageChangedMention(
13720
+ args.body,
13721
+ botUserId,
13722
+ slackAdapter
13723
+ );
13724
+ if (!result) {
13725
+ return false;
13726
+ }
13727
+ args.bot.processMessage(
13728
+ slackAdapter,
13729
+ result.threadId,
13730
+ result.message,
13731
+ webhookOptions
13732
+ );
13733
+ return true;
13734
+ };
13735
+ if (authAdapter.defaultBotToken) {
13736
+ dispatch();
13737
+ return;
13738
+ }
13739
+ const teamId = getSlackPayloadTeamId(args.body);
13740
+ if (!teamId || !authAdapter.resolveTokenForTeam || !authAdapter.requestContext) {
13741
+ return;
13742
+ }
13743
+ const context = await authAdapter.resolveTokenForTeam(teamId);
13744
+ if (!context) {
13745
+ return;
13746
+ }
13747
+ authAdapter.requestContext.run(context, dispatch);
13748
+ }
13749
+ async function handlePlatformWebhook(request, platform, waitUntil, bot = getProductionBot()) {
12980
13750
  const handler = bot.webhooks[platform];
12981
13751
  const requestContext = createRequestContext(request, { platform });
12982
13752
  const requestUrl = new URL(request.url);
@@ -12994,6 +13764,34 @@ async function POST(request, platform, waitUntil) {
12994
13764
  );
12995
13765
  return new Response(`Unknown platform: ${platform}`, { status: 404 });
12996
13766
  }
13767
+ let rebuiltRequest = request;
13768
+ if (platform === "slack") {
13769
+ const rawBody = await request.text();
13770
+ let parsedBody;
13771
+ try {
13772
+ parsedBody = JSON.parse(rawBody);
13773
+ } catch {
13774
+ parsedBody = void 0;
13775
+ }
13776
+ if (parsedBody && isMessageChangedEnvelope(parsedBody)) {
13777
+ try {
13778
+ await handleAuthenticatedSlackMessageChangedMention({
13779
+ body: parsedBody,
13780
+ bot,
13781
+ rawBody,
13782
+ request,
13783
+ waitUntil
13784
+ });
13785
+ } catch (error) {
13786
+ logException(error, "slack_message_changed_side_channel_failed");
13787
+ }
13788
+ }
13789
+ rebuiltRequest = new Request(request.url, {
13790
+ method: request.method,
13791
+ headers: request.headers,
13792
+ body: rawBody
13793
+ });
13794
+ }
12997
13795
  try {
12998
13796
  return await withSpan(
12999
13797
  "http.server.request",
@@ -13001,7 +13799,7 @@ async function POST(request, platform, waitUntil) {
13001
13799
  requestContext,
13002
13800
  async () => {
13003
13801
  try {
13004
- const response = await handler(request, {
13802
+ const response = await handler(rebuiltRequest, {
13005
13803
  waitUntil: (task) => waitUntil(task)
13006
13804
  });
13007
13805
  if (response.status >= 400) {
@@ -13047,6 +13845,9 @@ async function POST(request, platform, waitUntil) {
13047
13845
  }
13048
13846
  });
13049
13847
  }
13848
+ async function POST(request, platform, waitUntil) {
13849
+ return handlePlatformWebhook(request, platform, waitUntil);
13850
+ }
13050
13851
 
13051
13852
  // src/app.ts
13052
13853
  async function defaultWaitUntil() {