@sentry/junior 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -5,9 +5,8 @@ import {
5
5
  listCapabilityProviders,
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
- parseSkillInvocation,
9
- stripFrontmatter
10
- } from "./chunk-4XWTSMRF.js";
8
+ parseSkillInvocation
9
+ } from "./chunk-VJLT6LLV.js";
11
10
  import {
12
11
  SANDBOX_SKILLS_ROOT,
13
12
  SANDBOX_WORKSPACE_ROOT,
@@ -25,8 +24,9 @@ import {
25
24
  resolveRuntimeDependencySnapshot,
26
25
  runNonInteractiveCommand,
27
26
  sandboxSkillDir,
27
+ sandboxSkillFile,
28
28
  toOptionalTrimmed
29
- } from "./chunk-XYOKYK6U.js";
29
+ } from "./chunk-LOTYK7IE.js";
30
30
  import {
31
31
  CredentialUnavailableError,
32
32
  buildOAuthTokenRequest,
@@ -2999,6 +2999,8 @@ function buildSystemPrompt(params) {
2999
2999
  "- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before authenticated bash/API work for that skill.",
3000
3000
  "- Use the minimum declared capability needed for the current operation.",
3001
3001
  "- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
3002
+ "- For disconnect + reconnect requests, run `jr-rpc delete-token <provider>` first, then `jr-rpc issue-credential` \u2014 the system handles the reconnect without auto-resuming the reconnect message.",
3003
+ "- Use `jr-rpc oauth-start <provider>` only when the user explicitly asks to connect a provider and there is no task to resume after authorization.",
3002
3004
  "- GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
3003
3005
  "- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
3004
3006
  "- Capabilities are provider-qualified (for example `github.issues.write`).",
@@ -3539,6 +3541,52 @@ async function handleIssueCredentialCommand(args, deps) {
3539
3541
  });
3540
3542
  } catch (error) {
3541
3543
  if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
3544
+ const authAction = deps.providerAuthActions?.get(error.provider);
3545
+ if (authAction?.kind === "oauth_started") {
3546
+ const providerLabel = formatProviderLabel(error.provider);
3547
+ return commandResult({
3548
+ stdout: {
3549
+ credential_unavailable: true,
3550
+ oauth_started: true,
3551
+ provider: error.provider,
3552
+ private_delivery_sent: authAction.delivered,
3553
+ message: authAction.delivered ? `I've already sent you a private authorization link to connect your ${providerLabel} account. Finish that flow, then return to Slack.` : `I still need to connect your ${providerLabel} account, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
3554
+ },
3555
+ exitCode: 0
3556
+ });
3557
+ }
3558
+ if (authAction?.kind === "token_deleted") {
3559
+ const reconnectResult = await startOAuthFlow(error.provider, {
3560
+ requesterId: deps.requesterId,
3561
+ channelId: deps.channelId,
3562
+ threadTs: deps.threadTs,
3563
+ activeSkillName: deps.activeSkill?.name ?? void 0
3564
+ // Intentionally no userMessage — reconnect flows must not auto-resume.
3565
+ });
3566
+ if (!reconnectResult.ok) {
3567
+ return commandResult({
3568
+ stderr: `${reconnectResult.error}
3569
+ `,
3570
+ exitCode: 1
3571
+ });
3572
+ }
3573
+ const delivered = !!reconnectResult.delivery;
3574
+ deps.providerAuthActions?.set(error.provider, {
3575
+ kind: "oauth_started",
3576
+ delivered
3577
+ });
3578
+ const providerLabel = formatProviderLabel(error.provider);
3579
+ return commandResult({
3580
+ stdout: {
3581
+ credential_unavailable: true,
3582
+ oauth_started: true,
3583
+ provider: error.provider,
3584
+ private_delivery_sent: delivered,
3585
+ message: delivered ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
3586
+ },
3587
+ exitCode: 0
3588
+ });
3589
+ }
3542
3590
  const oauthResult = await startOAuthFlow(error.provider, {
3543
3591
  requesterId: deps.requesterId,
3544
3592
  channelId: deps.channelId,
@@ -3808,6 +3856,10 @@ async function handleOAuthStartCommand(args, deps) {
3808
3856
  return commandResult({ stderr: `${result.error}
3809
3857
  `, exitCode: 1 });
3810
3858
  }
3859
+ deps.providerAuthActions?.set(provider, {
3860
+ kind: "oauth_started",
3861
+ delivered: !!result.delivery
3862
+ });
3811
3863
  if (!result.delivery) {
3812
3864
  return commandResult({
3813
3865
  stdout: {
@@ -3854,6 +3906,7 @@ async function handleDeleteTokenCommand(args, deps) {
3854
3906
  });
3855
3907
  }
3856
3908
  await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
3909
+ deps.providerAuthActions?.set(provider, { kind: "token_deleted" });
3857
3910
  logInfo(
3858
3911
  "jr_rpc_delete_token",
3859
3912
  {},
@@ -4968,7 +5021,7 @@ function toLoadedSkill(result, availableSkills) {
4968
5021
  return {
4969
5022
  name: result.skill_name,
4970
5023
  description: result.description,
4971
- skillPath: result.skill_dir,
5024
+ skillPath: metadata?.skillPath ?? result.skill_dir,
4972
5025
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
4973
5026
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
4974
5027
  ...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
@@ -4976,7 +5029,7 @@ function toLoadedSkill(result, availableSkills) {
4976
5029
  body: result.instructions
4977
5030
  };
4978
5031
  }
4979
- async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5032
+ async function loadSkillFromHost(availableSkills, skillName) {
4980
5033
  const requested = skillName.trim().toLowerCase();
4981
5034
  const skill = availableSkills.find(
4982
5035
  (entry) => entry.name.toLowerCase() === requested
@@ -4989,10 +5042,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
4989
5042
  };
4990
5043
  }
4991
5044
  const skillDir = sandboxSkillDir(skill.name);
4992
- const skillFilePath = `${skillDir}/SKILL.md`;
4993
- const file = await sandbox.readFileToBuffer({ path: skillFilePath });
4994
- if (!file) {
4995
- throw new Error(`failed to read ${skillFilePath}`);
5045
+ const skillFilePath = sandboxSkillFile(skill.name);
5046
+ const [loaded] = await loadSkillsByName([skill.name], availableSkills);
5047
+ if (!loaded) {
5048
+ throw new Error(`failed to load ${skill.name}`);
4996
5049
  }
4997
5050
  return {
4998
5051
  ok: true,
@@ -5001,10 +5054,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
5001
5054
  ...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
5002
5055
  skill_dir: skillDir,
5003
5056
  location: skillFilePath,
5004
- instructions: stripFrontmatter(file.toString("utf8"))
5057
+ instructions: loaded.body
5005
5058
  };
5006
5059
  }
5007
- function createLoadSkillTool(sandbox, availableSkills, options) {
5060
+ function createLoadSkillTool(availableSkills, options) {
5008
5061
  return tool({
5009
5062
  description: "Load a skill by name so its instructions are available for this turn. The result includes `requires_capabilities` when the skill declares authenticated provider access, and `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
5010
5063
  inputSchema: Type4.Object({
@@ -5014,11 +5067,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
5014
5067
  })
5015
5068
  }),
5016
5069
  execute: async ({ skill_name }) => {
5017
- const result = await loadSkillFromSandbox(
5018
- sandbox,
5019
- availableSkills,
5020
- skill_name
5021
- );
5070
+ const result = await loadSkillFromHost(availableSkills, skill_name);
5022
5071
  const loadedSkill = toLoadedSkill(result, availableSkills);
5023
5072
  if (loadedSkill) {
5024
5073
  const metadata = await options?.onSkillLoaded?.(loadedSkill);
@@ -6754,103 +6803,50 @@ function createToolState(hooks, context) {
6754
6803
  }
6755
6804
  };
6756
6805
  }
6757
- function wrapToolExecution(toolName, toolDef, hooks) {
6758
- const maybeExecutable = toolDef;
6759
- if (!maybeExecutable.execute) {
6760
- return toolDef;
6761
- }
6762
- const originalExecute = maybeExecutable.execute.bind(toolDef);
6763
- maybeExecutable.execute = async (...args) => {
6764
- const input = args[0];
6765
- await hooks.onToolCallStart?.(toolName, input);
6766
- return originalExecute(...args);
6767
- };
6768
- return toolDef;
6769
- }
6770
6806
  function createTools(availableSkills, hooks = {}, context) {
6771
6807
  const state = createToolState(hooks, context);
6772
6808
  const tools = {
6773
- loadSkill: wrapToolExecution(
6774
- "loadSkill",
6775
- createLoadSkillTool(context.sandbox, availableSkills, {
6776
- onSkillLoaded: hooks.onSkillLoaded
6777
- }),
6778
- hooks
6779
- ),
6780
- systemTime: wrapToolExecution("systemTime", createSystemTimeTool(), hooks),
6781
- bash: wrapToolExecution("bash", createBashTool(), hooks),
6782
- attachFile: wrapToolExecution(
6783
- "attachFile",
6784
- createAttachFileTool(context.sandbox, hooks),
6785
- hooks
6786
- ),
6787
- readFile: wrapToolExecution("readFile", createReadFileTool(), hooks),
6788
- writeFile: wrapToolExecution("writeFile", createWriteFileTool(), hooks),
6789
- webSearch: wrapToolExecution("webSearch", createWebSearchTool(), hooks),
6790
- webFetch: wrapToolExecution("webFetch", createWebFetchTool(hooks), hooks),
6791
- imageGenerate: wrapToolExecution(
6792
- "imageGenerate",
6793
- createImageGenerateTool(hooks, hooks.toolOverrides?.imageGenerate),
6794
- hooks
6795
- ),
6796
- slackCanvasUpdate: wrapToolExecution(
6797
- "slackCanvasUpdate",
6798
- createSlackCanvasUpdateTool(state, context),
6799
- hooks
6800
- ),
6801
- slackListCreate: wrapToolExecution(
6802
- "slackListCreate",
6803
- createSlackListCreateTool(state),
6804
- hooks
6805
- ),
6806
- slackListAddItems: wrapToolExecution(
6807
- "slackListAddItems",
6808
- createSlackListAddItemsTool(state),
6809
- hooks
6810
- ),
6811
- slackListGetItems: wrapToolExecution(
6812
- "slackListGetItems",
6813
- createSlackListGetItemsTool(state),
6814
- hooks
6809
+ loadSkill: createLoadSkillTool(availableSkills, {
6810
+ onSkillLoaded: hooks.onSkillLoaded
6811
+ }),
6812
+ systemTime: createSystemTimeTool(),
6813
+ bash: createBashTool(),
6814
+ attachFile: createAttachFileTool(context.sandbox, hooks),
6815
+ readFile: createReadFileTool(),
6816
+ writeFile: createWriteFileTool(),
6817
+ webSearch: createWebSearchTool(),
6818
+ webFetch: createWebFetchTool(hooks),
6819
+ imageGenerate: createImageGenerateTool(
6820
+ hooks,
6821
+ hooks.toolOverrides?.imageGenerate
6815
6822
  ),
6816
- slackListUpdateItem: wrapToolExecution(
6817
- "slackListUpdateItem",
6818
- createSlackListUpdateItemTool(state),
6819
- hooks
6820
- )
6823
+ slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
6824
+ slackListCreate: createSlackListCreateTool(state),
6825
+ slackListAddItems: createSlackListAddItemsTool(state),
6826
+ slackListGetItems: createSlackListGetItemsTool(state),
6827
+ slackListUpdateItem: createSlackListUpdateItemTool(state)
6821
6828
  };
6822
6829
  if (context.mcpToolManager && context.getActiveSkills) {
6823
- tools.searchTools = wrapToolExecution(
6824
- "searchTools",
6825
- createSearchToolsTool(context.mcpToolManager, context.getActiveSkills),
6826
- hooks
6830
+ tools.searchTools = createSearchToolsTool(
6831
+ context.mcpToolManager,
6832
+ context.getActiveSkills
6827
6833
  );
6828
6834
  }
6829
6835
  const { channelCapabilities } = context;
6830
6836
  if (channelCapabilities.canCreateCanvas) {
6831
- tools.slackCanvasCreate = wrapToolExecution(
6832
- "slackCanvasCreate",
6833
- createSlackCanvasCreateTool(context, state),
6834
- hooks
6835
- );
6837
+ tools.slackCanvasCreate = createSlackCanvasCreateTool(context, state);
6836
6838
  }
6837
6839
  if (channelCapabilities.canPostToChannel) {
6838
- tools.slackChannelPostMessage = wrapToolExecution(
6839
- "slackChannelPostMessage",
6840
- createSlackChannelPostMessageTool(context, state),
6841
- hooks
6842
- );
6843
- tools.slackChannelListMessages = wrapToolExecution(
6844
- "slackChannelListMessages",
6845
- createSlackChannelListMessagesTool(context),
6846
- hooks
6840
+ tools.slackChannelPostMessage = createSlackChannelPostMessageTool(
6841
+ context,
6842
+ state
6847
6843
  );
6844
+ tools.slackChannelListMessages = createSlackChannelListMessagesTool(context);
6848
6845
  }
6849
6846
  if (channelCapabilities.canAddReactions) {
6850
- tools.slackMessageAddReaction = wrapToolExecution(
6851
- "slackMessageAddReaction",
6852
- createSlackMessageAddReactionTool(context, state),
6853
- hooks
6847
+ tools.slackMessageAddReaction = createSlackMessageAddReactionTool(
6848
+ context,
6849
+ state
6854
6850
  );
6855
6851
  }
6856
6852
  return tools;
@@ -6866,10 +6862,7 @@ function resolveChannelCapabilities(channelId) {
6866
6862
  }
6867
6863
 
6868
6864
  // src/chat/sandbox/sandbox.ts
6869
- import fs3 from "fs/promises";
6870
- import path4 from "path";
6871
- import { Sandbox } from "@vercel/sandbox";
6872
- import { createBashTool as createBashTool2 } from "bash-tool";
6865
+ import fs4 from "fs/promises";
6873
6866
 
6874
6867
  // src/chat/sandbox/http-error-details.ts
6875
6868
  var DEFAULT_PREVIEW_LIMIT = 512;
@@ -6973,14 +6966,7 @@ function extractHttpErrorDetails(error, options = {}) {
6973
6966
  };
6974
6967
  }
6975
6968
 
6976
- // src/chat/sandbox/sandbox.ts
6977
- var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
6978
- var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
6979
- var SANDBOX_RUNTIME = "node22";
6980
- var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
6981
- var EVAL_GH_STUB_PATH = `${SANDBOX_RUNTIME_BIN_DIR}/gh`;
6982
- var SNAPSHOT_BOOT_RETRY_COUNT = 3;
6983
- var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
6969
+ // src/chat/sandbox/errors.ts
6984
6970
  var SANDBOX_ERROR_FIELDS = [
6985
6971
  {
6986
6972
  sourceKey: "sandboxId",
@@ -6988,140 +6974,410 @@ var SANDBOX_ERROR_FIELDS = [
6988
6974
  summaryKey: "sandboxId"
6989
6975
  }
6990
6976
  ];
6991
- function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
6992
- const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
6993
- const existingAllowRaw = basePolicy.allow;
6994
- const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
6995
- Object.entries(existingAllowRaw).map(
6996
- ([domain, rules]) => [
6997
- domain,
6998
- Array.isArray(rules) ? [...rules] : []
6999
- ]
7000
- )
7001
- ) : { "*": [] };
7002
- for (const transform of headerTransforms) {
7003
- const currentRules = existingAllow[transform.domain] ?? [];
7004
- existingAllow[transform.domain] = [
7005
- ...currentRules,
7006
- { transform: [{ headers: transform.headers }] }
7007
- ];
7008
- }
7009
- return {
7010
- ...basePolicy,
7011
- allow: existingAllow
7012
- };
6977
+ function getSandboxErrorDetails(error) {
6978
+ return extractHttpErrorDetails(error, {
6979
+ attributePrefix: "app.sandbox.api_error",
6980
+ extraFields: [...SANDBOX_ERROR_FIELDS]
6981
+ });
7013
6982
  }
7014
- function truncateOutput(output, maxLength) {
7015
- if (output.length <= maxLength) {
7016
- return { value: output, truncated: false };
6983
+ function findInErrorChain(error, predicate) {
6984
+ const seen = /* @__PURE__ */ new Set();
6985
+ let current = error;
6986
+ while (current && !seen.has(current)) {
6987
+ if (predicate(current)) {
6988
+ return true;
6989
+ }
6990
+ seen.add(current);
6991
+ current = typeof current === "object" ? current.cause : void 0;
7017
6992
  }
7018
- const truncatedLength = output.length - maxLength;
7019
- return {
7020
- value: `${output.slice(0, maxLength)}
7021
-
7022
- [output truncated: ${truncatedLength} characters removed]`,
7023
- truncated: true
7024
- };
7025
- }
7026
- function toPosixRelative(base, absolute) {
7027
- return path4.relative(base, absolute).split(path4.sep).join("/");
6993
+ return false;
7028
6994
  }
7029
- async function listFilesRecursive(root) {
7030
- const queue = [root];
7031
- const files = [];
7032
- while (queue.length > 0) {
7033
- const dir = queue.shift();
7034
- const entries = await fs3.readdir(dir, { withFileTypes: true });
7035
- entries.sort((a, b) => a.name.localeCompare(b.name));
7036
- for (const entry of entries) {
7037
- const absolute = path4.join(dir, entry.name);
7038
- if (entry.isDirectory()) {
7039
- queue.push(absolute);
7040
- } else if (entry.isFile()) {
7041
- files.push(absolute);
6995
+ function getFirstErrorMessage(error) {
6996
+ const seen = /* @__PURE__ */ new Set();
6997
+ let current = error;
6998
+ while (current && !seen.has(current)) {
6999
+ if (current instanceof Error) {
7000
+ const message = current.message.trim();
7001
+ if (message) {
7002
+ return message;
7042
7003
  }
7043
7004
  }
7005
+ seen.add(current);
7006
+ current = typeof current === "object" ? current.cause : void 0;
7044
7007
  }
7045
- return files;
7008
+ return void 0;
7046
7009
  }
7047
- async function buildSkillSyncFiles(availableSkills) {
7048
- const filesToWrite = [];
7049
- const index = {
7050
- skills: []
7051
- };
7052
- for (const skill of availableSkills) {
7053
- const skillFiles = await listFilesRecursive(skill.skillPath);
7054
- for (const absoluteFile of skillFiles) {
7055
- const relative = toPosixRelative(skill.skillPath, absoluteFile);
7056
- if (!relative || relative.startsWith("..")) {
7057
- continue;
7058
- }
7059
- filesToWrite.push({
7060
- path: `${sandboxSkillDir(skill.name)}/${relative}`,
7061
- content: await fs3.readFile(absoluteFile)
7010
+ function isAlreadyExistsError(error) {
7011
+ const details = getSandboxErrorDetails(error);
7012
+ return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7013
+ }
7014
+ function isSandboxUnavailableError(error) {
7015
+ return findInErrorChain(error, (candidate) => {
7016
+ const details = getSandboxErrorDetails(candidate);
7017
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7018
+ return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7019
+ });
7020
+ }
7021
+ function isSnapshottingError(error) {
7022
+ return findInErrorChain(error, (candidate) => {
7023
+ const details = getSandboxErrorDetails(candidate);
7024
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7025
+ return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7026
+ });
7027
+ }
7028
+ function wrapSandboxSetupError(error) {
7029
+ try {
7030
+ const details = getSandboxErrorDetails(error);
7031
+ if (details.summary) {
7032
+ return new Error(`sandbox setup failed (${details.summary})`, {
7033
+ cause: error
7062
7034
  });
7063
7035
  }
7064
- index.skills.push({
7065
- name: skill.name,
7066
- description: skill.description,
7067
- root: sandboxSkillDir(skill.name)
7068
- });
7036
+ } catch {
7069
7037
  }
7070
- filesToWrite.push({
7071
- path: `${SANDBOX_SKILLS_ROOT}/index.json`,
7072
- content: Buffer.from(JSON.stringify(index), "utf8")
7073
- });
7074
- if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
7075
- filesToWrite.push({
7076
- path: EVAL_GH_STUB_PATH,
7077
- content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
7078
- });
7038
+ let causeMessage;
7039
+ try {
7040
+ causeMessage = getFirstErrorMessage(error);
7041
+ } catch (cause) {
7042
+ causeMessage = cause instanceof Error ? cause.message : void 0;
7079
7043
  }
7080
- return filesToWrite;
7044
+ if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7045
+ const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7046
+ return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7047
+ }
7048
+ return new Error("sandbox setup failed", { cause: error });
7049
+ }
7050
+ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7051
+ const details = getSandboxErrorDetails(error);
7052
+ setSpanAttributes({
7053
+ ...details.attributes,
7054
+ ...includeMissingPath ? {
7055
+ "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7056
+ } : {},
7057
+ "app.sandbox.success": false
7058
+ });
7059
+ setSpanStatus("error");
7060
+ throw new Error(
7061
+ details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7062
+ {
7063
+ cause: error
7064
+ }
7065
+ );
7081
7066
  }
7082
- function buildEvalGitHubCliStub() {
7083
- return `#!/usr/bin/env node
7084
- const fs = require("node:fs");
7085
- const path = require("node:path");
7086
- const { spawnSync } = require("node:child_process");
7087
7067
 
7088
- const args = process.argv.slice(2);
7089
- const statePath = "/vercel/sandbox/.junior/eval-gh-state.json";
7090
- const fallbackBinaries = ["/usr/bin/gh", "/usr/local/bin/gh", "/bin/gh"];
7091
- const flagsWithValues = new Set([
7092
- "--repo",
7093
- "--title",
7094
- "--body",
7095
- "--body-file",
7096
- "--json",
7097
- "--search",
7098
- "--state",
7099
- "--limit",
7100
- "--method",
7101
- "--jq",
7102
- "--template",
7103
- "--hostname",
7104
- ]);
7068
+ // src/chat/sandbox/session.ts
7069
+ import { Sandbox } from "@vercel/sandbox";
7070
+ import { createBashTool as createBashTool2 } from "bash-tool";
7105
7071
 
7106
- function getFlag(name) {
7107
- for (let index = 0; index < args.length; index += 1) {
7108
- const value = args[index];
7109
- if (value === name) {
7110
- return args[index + 1];
7111
- }
7112
- if (value.startsWith(name + "=")) {
7113
- return value.slice(name.length + 1);
7114
- }
7072
+ // src/chat/runtime/status-format.ts
7073
+ var SLACK_STATUS_MAX_LENGTH = 50;
7074
+ function truncateWithEllipsis(text, maxLength) {
7075
+ if (text.length <= maxLength) {
7076
+ return text;
7115
7077
  }
7116
- return undefined;
7078
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
7117
7079
  }
7118
-
7119
- function getPositionals() {
7120
- const values = [];
7121
- for (let index = 0; index < args.length; index += 1) {
7122
- const value = args[index];
7123
- if (flagsWithValues.has(value)) {
7124
- index += 1;
7080
+ function truncateStatusText(text) {
7081
+ const trimmed = text.trim();
7082
+ if (!trimmed) {
7083
+ return "";
7084
+ }
7085
+ return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
7086
+ }
7087
+ function compactStatusPath(value) {
7088
+ if (typeof value !== "string") {
7089
+ return void 0;
7090
+ }
7091
+ const trimmed = value.trim();
7092
+ if (!trimmed) {
7093
+ return void 0;
7094
+ }
7095
+ if (trimmed.length <= 80) {
7096
+ return trimmed;
7097
+ }
7098
+ return `...${trimmed.slice(-77)}`;
7099
+ }
7100
+ function compactStatusText(value, maxLength = 80) {
7101
+ if (typeof value !== "string") {
7102
+ return void 0;
7103
+ }
7104
+ const trimmed = value.trim();
7105
+ if (!trimmed) {
7106
+ return void 0;
7107
+ }
7108
+ return truncateWithEllipsis(trimmed, maxLength);
7109
+ }
7110
+ function readShellToken(command, startIndex) {
7111
+ let index = startIndex;
7112
+ while (index < command.length && /\s/.test(command[index] ?? "")) {
7113
+ index += 1;
7114
+ }
7115
+ if (index >= command.length) {
7116
+ return void 0;
7117
+ }
7118
+ let token = "";
7119
+ let quote;
7120
+ while (index < command.length) {
7121
+ const char = command[index];
7122
+ if (!char) {
7123
+ break;
7124
+ }
7125
+ if (quote) {
7126
+ if (char === quote) {
7127
+ quote = void 0;
7128
+ index += 1;
7129
+ continue;
7130
+ }
7131
+ if (char === "\\" && quote === '"' && index + 1 < command.length) {
7132
+ token += command[index + 1];
7133
+ index += 2;
7134
+ continue;
7135
+ }
7136
+ token += char;
7137
+ index += 1;
7138
+ continue;
7139
+ }
7140
+ if (/\s/.test(char)) {
7141
+ break;
7142
+ }
7143
+ if (char === '"' || char === "'") {
7144
+ quote = char;
7145
+ index += 1;
7146
+ continue;
7147
+ }
7148
+ if (char === "\\" && index + 1 < command.length) {
7149
+ token += command[index + 1];
7150
+ index += 2;
7151
+ continue;
7152
+ }
7153
+ token += char;
7154
+ index += 1;
7155
+ }
7156
+ return { token, nextIndex: index };
7157
+ }
7158
+ function compactStatusCommand(value) {
7159
+ if (typeof value !== "string") {
7160
+ return void 0;
7161
+ }
7162
+ const trimmed = value.trim();
7163
+ if (!trimmed) {
7164
+ return void 0;
7165
+ }
7166
+ let index = 0;
7167
+ while (index < trimmed.length) {
7168
+ const parsed = readShellToken(trimmed, index);
7169
+ if (!parsed) {
7170
+ return void 0;
7171
+ }
7172
+ index = parsed.nextIndex;
7173
+ if (!parsed.token) {
7174
+ continue;
7175
+ }
7176
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
7177
+ continue;
7178
+ }
7179
+ const normalized = parsed.token.replace(/[\\/]+$/g, "");
7180
+ if (!normalized) {
7181
+ return void 0;
7182
+ }
7183
+ const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
7184
+ const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
7185
+ return compactStatusText(command, 40);
7186
+ }
7187
+ return void 0;
7188
+ }
7189
+ function compactStatusFilename(value) {
7190
+ if (typeof value !== "string") {
7191
+ return void 0;
7192
+ }
7193
+ const trimmed = value.trim().replace(/[\\/]+$/g, "");
7194
+ if (!trimmed) {
7195
+ return void 0;
7196
+ }
7197
+ const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
7198
+ const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
7199
+ return compactStatusText(filename, 80);
7200
+ }
7201
+ function extractStatusUrlDomain(value) {
7202
+ if (typeof value !== "string") {
7203
+ return void 0;
7204
+ }
7205
+ const trimmed = value.trim();
7206
+ if (!trimmed) {
7207
+ return void 0;
7208
+ }
7209
+ try {
7210
+ const parsed = new URL(trimmed);
7211
+ return parsed.hostname || void 0;
7212
+ } catch {
7213
+ return void 0;
7214
+ }
7215
+ }
7216
+
7217
+ // src/chat/runtime/assistant-status.ts
7218
+ var STATUS_PATTERNS = {
7219
+ thinking: {
7220
+ defaultContext: "\u2026",
7221
+ variants: ["Thinking", "Reasoning", "Considering", "Working through"]
7222
+ },
7223
+ searching: {
7224
+ defaultContext: "sources",
7225
+ variants: ["Searching", "Scanning", "Probing", "Trawling"]
7226
+ },
7227
+ reading: {
7228
+ defaultContext: "task",
7229
+ variants: ["Reading", "Inspecting", "Parsing", "Skimming"]
7230
+ },
7231
+ reviewing: {
7232
+ defaultContext: "results",
7233
+ variants: ["Reviewing", "Checking", "Inspecting", "Auditing"]
7234
+ },
7235
+ loading: {
7236
+ defaultContext: "task",
7237
+ variants: ["Loading", "Priming", "Booting", "Spinning up"]
7238
+ },
7239
+ updating: {
7240
+ defaultContext: "state",
7241
+ variants: ["Updating", "Patching", "Refreshing", "Adjusting"]
7242
+ },
7243
+ fetching: {
7244
+ defaultContext: "sources",
7245
+ variants: ["Fetching", "Pulling", "Retrieving", "Loading"]
7246
+ },
7247
+ creating: {
7248
+ defaultContext: "draft",
7249
+ variants: ["Creating", "Building", "Assembling", "Generating"]
7250
+ },
7251
+ listing: {
7252
+ defaultContext: "items",
7253
+ variants: ["Listing", "Gathering", "Collecting", "Enumerating"]
7254
+ },
7255
+ posting: {
7256
+ defaultContext: "reply",
7257
+ variants: ["Posting", "Sending", "Delivering", "Dispatching"]
7258
+ },
7259
+ adding: {
7260
+ defaultContext: "details",
7261
+ variants: ["Adding", "Applying", "Attaching", "Dropping in"]
7262
+ },
7263
+ running: {
7264
+ defaultContext: "tasks",
7265
+ variants: ["Running", "Executing", "Launching", "Processing"]
7266
+ }
7267
+ };
7268
+ function makeAssistantStatus(kind, context) {
7269
+ return { kind, ...context ? { context } : {} };
7270
+ }
7271
+ function normalizeAssistantStatusText(text) {
7272
+ const trimmed = text.trim();
7273
+ if (!trimmed) {
7274
+ return "";
7275
+ }
7276
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
7277
+ }
7278
+ function buildAssistantStatusPresentation(args) {
7279
+ const random = args.random ?? Math.random;
7280
+ const pattern = STATUS_PATTERNS[args.status.kind];
7281
+ const context = normalizeAssistantStatusText(args.status.context ?? "") || pattern.defaultContext;
7282
+ const index = Math.floor(random() * pattern.variants.length);
7283
+ const verb = pattern.variants[index] ?? pattern.variants[0];
7284
+ const visible = truncateStatusText(`${verb} ${context}`);
7285
+ const hint = truncateStatusText(`${pattern.variants[0]} ${context}`);
7286
+ return {
7287
+ key: `${args.status.kind}:${context}`,
7288
+ hint,
7289
+ visible,
7290
+ suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7291
+ };
7292
+ }
7293
+ function createSlackAdapterAssistantStatusTransport(args) {
7294
+ return {
7295
+ async setStatus(channelId, threadTs, status, suggestions) {
7296
+ try {
7297
+ await args.getSlackAdapter().setAssistantStatus(channelId, threadTs, status, suggestions);
7298
+ } catch (error) {
7299
+ logAssistantStatusFailure(status, error);
7300
+ }
7301
+ }
7302
+ };
7303
+ }
7304
+ function createSlackWebApiAssistantStatusTransport(args) {
7305
+ const getClient2 = args?.getSlackClient ?? getSlackClient;
7306
+ return {
7307
+ async setStatus(channelId, threadTs, status, suggestions) {
7308
+ try {
7309
+ await getClient2().assistant.threads.setStatus({
7310
+ channel_id: channelId,
7311
+ thread_ts: threadTs,
7312
+ status,
7313
+ ...suggestions ? { loading_messages: suggestions } : {}
7314
+ });
7315
+ } catch (error) {
7316
+ logAssistantStatusFailure(status, error);
7317
+ }
7318
+ }
7319
+ };
7320
+ }
7321
+ function logAssistantStatusFailure(status, error) {
7322
+ logWarn(
7323
+ "assistant_status_update_failed",
7324
+ {},
7325
+ {
7326
+ "app.slack.status_text": status || "(clear)",
7327
+ "error.message": error instanceof Error ? error.message : String(error)
7328
+ },
7329
+ "Failed to update assistant status"
7330
+ );
7331
+ }
7332
+
7333
+ // src/chat/sandbox/skill-sync.ts
7334
+ import fs3 from "fs/promises";
7335
+ import path4 from "path";
7336
+
7337
+ // src/chat/sandbox/eval-gh-stub.ts
7338
+ function buildEvalGitHubCliStub() {
7339
+ return `#!/usr/bin/env node
7340
+ const fs = require("node:fs");
7341
+ const path = require("node:path");
7342
+ const { spawnSync } = require("node:child_process");
7343
+
7344
+ const args = process.argv.slice(2);
7345
+ const statePath = "/vercel/sandbox/.junior/eval-gh-state.json";
7346
+ const fallbackBinaries = ["/usr/bin/gh", "/usr/local/bin/gh", "/bin/gh"];
7347
+ const flagsWithValues = new Set([
7348
+ "--repo",
7349
+ "--title",
7350
+ "--body",
7351
+ "--body-file",
7352
+ "--json",
7353
+ "--search",
7354
+ "--state",
7355
+ "--limit",
7356
+ "--method",
7357
+ "--jq",
7358
+ "--template",
7359
+ "--hostname",
7360
+ ]);
7361
+
7362
+ function getFlag(name) {
7363
+ for (let index = 0; index < args.length; index += 1) {
7364
+ const value = args[index];
7365
+ if (value === name) {
7366
+ return args[index + 1];
7367
+ }
7368
+ if (value.startsWith(name + "=")) {
7369
+ return value.slice(name.length + 1);
7370
+ }
7371
+ }
7372
+ return undefined;
7373
+ }
7374
+
7375
+ function getPositionals() {
7376
+ const values = [];
7377
+ for (let index = 0; index < args.length; index += 1) {
7378
+ const value = args[index];
7379
+ if (flagsWithValues.has(value)) {
7380
+ index += 1;
7125
7381
  continue;
7126
7382
  }
7127
7383
  if (value.startsWith("--") && value.includes("=")) {
@@ -7256,6 +7512,8 @@ if (args[0] === "api") {
7256
7512
  outputJson({ items: [] });
7257
7513
  process.exit(0);
7258
7514
  }
7515
+ outputJson({});
7516
+ process.exit(0);
7259
7517
  }
7260
7518
 
7261
7519
  if (args[0] === "issue") {
@@ -7297,7 +7555,9 @@ if (args[0] === "issue") {
7297
7555
 
7298
7556
  const number = Number.parseInt(positionals[2] || "", 10);
7299
7557
  const key = repo + "#" + number;
7300
- const record = state.issues[key] || defaultIssue(repo, Number.isFinite(number) ? number : 101);
7558
+ const record =
7559
+ state.issues[key] ||
7560
+ defaultIssue(repo, Number.isFinite(number) ? number : 101);
7301
7561
 
7302
7562
  if (subcommand === "view") {
7303
7563
  const jsonFields = getFlag("--json");
@@ -7338,156 +7598,315 @@ if (args[0] === "issue") {
7338
7598
  fallbackToRealGh();
7339
7599
  `;
7340
7600
  }
7341
- function collectDirectories(filesToWrite) {
7342
- const directoriesToEnsure = /* @__PURE__ */ new Set();
7343
- for (const file of filesToWrite) {
7344
- const normalizedPath = path4.posix.normalize(file.path);
7345
- const parts = normalizedPath.split("/").filter(Boolean);
7346
- let current = "";
7347
- for (let index = 0; index < parts.length - 1; index += 1) {
7348
- current = `${current}/${parts[index]}`;
7349
- directoriesToEnsure.add(current);
7601
+
7602
+ // src/chat/sandbox/skill-sync.ts
7603
+ function toPosixRelative(base, absolute) {
7604
+ return path4.relative(base, absolute).split(path4.sep).join("/");
7605
+ }
7606
+ async function listFilesRecursive(root) {
7607
+ const queue = [root];
7608
+ const files = [];
7609
+ while (queue.length > 0) {
7610
+ const dir = queue.shift();
7611
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
7612
+ entries.sort((a, b) => a.name.localeCompare(b.name));
7613
+ for (const entry of entries) {
7614
+ const absolute = path4.join(dir, entry.name);
7615
+ if (entry.isDirectory()) {
7616
+ queue.push(absolute);
7617
+ } else if (entry.isFile()) {
7618
+ files.push(absolute);
7619
+ }
7350
7620
  }
7351
7621
  }
7352
- return Array.from(directoriesToEnsure).filter(
7353
- (directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
7354
- ).sort((a, b) => a.length - b.length);
7355
- }
7356
- function getSandboxErrorDetails(error) {
7357
- return extractHttpErrorDetails(error, {
7358
- attributePrefix: "app.sandbox.api_error",
7359
- extraFields: [...SANDBOX_ERROR_FIELDS]
7360
- });
7622
+ return files;
7361
7623
  }
7362
- function sleep2(ms) {
7363
- return new Promise((resolve) => {
7364
- setTimeout(resolve, ms);
7624
+ async function buildSkillSyncFiles(availableSkills, runtimeBinDir) {
7625
+ const filesToWrite = [];
7626
+ const index = {
7627
+ skills: []
7628
+ };
7629
+ for (const skill of availableSkills) {
7630
+ const skillFiles = await listFilesRecursive(skill.skillPath);
7631
+ for (const absoluteFile of skillFiles) {
7632
+ const relative = toPosixRelative(skill.skillPath, absoluteFile);
7633
+ if (!relative || relative.startsWith("..")) {
7634
+ continue;
7635
+ }
7636
+ filesToWrite.push({
7637
+ path: `${sandboxSkillDir(skill.name)}/${relative}`,
7638
+ content: await fs3.readFile(absoluteFile)
7639
+ });
7640
+ }
7641
+ index.skills.push({
7642
+ name: skill.name,
7643
+ description: skill.description,
7644
+ root: sandboxSkillDir(skill.name)
7645
+ });
7646
+ }
7647
+ filesToWrite.push({
7648
+ path: `${SANDBOX_SKILLS_ROOT}/index.json`,
7649
+ content: Buffer.from(JSON.stringify(index), "utf8")
7365
7650
  });
7651
+ if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
7652
+ filesToWrite.push({
7653
+ path: `${runtimeBinDir}/gh`,
7654
+ content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
7655
+ });
7656
+ }
7657
+ return filesToWrite;
7366
7658
  }
7367
- function isAlreadyExistsError(error) {
7368
- const details = getSandboxErrorDetails(error);
7369
- return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7659
+ function collectDirectories(filesToWrite, workspaceRoot) {
7660
+ const directoriesToEnsure = /* @__PURE__ */ new Set();
7661
+ for (const file of filesToWrite) {
7662
+ const normalizedPath = path4.posix.normalize(file.path);
7663
+ const parts = normalizedPath.split("/").filter(Boolean);
7664
+ let current = "";
7665
+ for (let index = 0; index < parts.length - 1; index += 1) {
7666
+ current = `${current}/${parts[index]}`;
7667
+ directoriesToEnsure.add(current);
7668
+ }
7669
+ }
7670
+ return Array.from(directoriesToEnsure).filter(
7671
+ (directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
7672
+ ).sort((a, b) => a.length - b.length);
7370
7673
  }
7371
- function findInErrorChain(error, predicate) {
7372
- const seen = /* @__PURE__ */ new Set();
7373
- let current = error;
7374
- while (current && !seen.has(current)) {
7375
- if (predicate(current)) {
7376
- return true;
7674
+ function resolveHostSkillPath(availableSkills, sandboxPath) {
7675
+ const normalizedPath = path4.posix.normalize(sandboxPath.trim());
7676
+ for (const skill of availableSkills) {
7677
+ const virtualRoot = sandboxSkillDir(skill.name);
7678
+ if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
7679
+ continue;
7377
7680
  }
7378
- seen.add(current);
7379
- if (typeof current === "object") {
7380
- current = current.cause;
7381
- } else {
7382
- current = void 0;
7681
+ const relativePath = path4.posix.relative(virtualRoot, normalizedPath);
7682
+ if (!relativePath || relativePath.startsWith("../")) {
7683
+ return null;
7383
7684
  }
7685
+ const hostRoot = path4.resolve(skill.skillPath);
7686
+ const hostPath = path4.resolve(hostRoot, ...relativePath.split("/"));
7687
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path4.sep}`)) {
7688
+ return null;
7689
+ }
7690
+ return hostPath;
7384
7691
  }
7385
- return false;
7386
- }
7387
- function isSandboxUnavailableError(error) {
7388
- return findInErrorChain(error, (candidate) => {
7389
- const details = getSandboxErrorDetails(candidate);
7390
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7391
- return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7392
- });
7692
+ return null;
7393
7693
  }
7394
- function isSnapshottingError(error) {
7395
- return findInErrorChain(error, (candidate) => {
7396
- const details = getSandboxErrorDetails(candidate);
7397
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7398
- return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7399
- });
7694
+ function isHostFileMissingError(error) {
7695
+ return Boolean(
7696
+ error && typeof error === "object" && error.code === "ENOENT"
7697
+ );
7400
7698
  }
7401
- function getFirstErrorMessage(error) {
7402
- const seen = /* @__PURE__ */ new Set();
7403
- let current = error;
7404
- while (current && !seen.has(current)) {
7405
- if (current instanceof Error) {
7406
- const message = current.message.trim();
7407
- if (message) {
7408
- return message;
7409
- }
7699
+ async function syncSkillsToSandbox(params) {
7700
+ const workspaceRoot = params.workspaceRoot ?? SANDBOX_WORKSPACE_ROOT;
7701
+ await params.withSpan(
7702
+ "sandbox.sync_skills",
7703
+ "sandbox.sync",
7704
+ {
7705
+ "app.sandbox.skills_count": params.skills.length
7706
+ },
7707
+ async () => {
7708
+ const filesToWrite = await buildSkillSyncFiles(
7709
+ params.skills,
7710
+ params.runtimeBinDir
7711
+ );
7712
+ const bytesWritten = filesToWrite.reduce(
7713
+ (total, file) => total + file.content.length,
7714
+ 0
7715
+ );
7716
+ const directories = collectDirectories(filesToWrite, workspaceRoot);
7717
+ await params.withSpan(
7718
+ "sandbox.sync_writeFiles",
7719
+ "sandbox.sync.write",
7720
+ {
7721
+ "app.sandbox.sync.files_written": filesToWrite.length,
7722
+ "app.sandbox.sync.bytes_written": bytesWritten,
7723
+ "app.sandbox.sync.directories_ensured": directories.length
7724
+ },
7725
+ async () => {
7726
+ try {
7727
+ for (const directory of directories) {
7728
+ try {
7729
+ await params.sandbox.mkDir(directory);
7730
+ } catch (error) {
7731
+ if (!isAlreadyExistsError(error)) {
7732
+ throw error;
7733
+ }
7734
+ }
7735
+ }
7736
+ await params.sandbox.writeFiles(filesToWrite);
7737
+ const executableFiles = filesToWrite.map((file) => file.path).filter(
7738
+ (filePath) => filePath.startsWith(`${params.runtimeBinDir}/`)
7739
+ );
7740
+ for (const filePath of executableFiles) {
7741
+ const chmod = await runNonInteractiveCommand(params.sandbox, {
7742
+ cmd: "chmod",
7743
+ args: ["0755", filePath],
7744
+ cwd: workspaceRoot
7745
+ });
7746
+ if (chmod.exitCode !== 0) {
7747
+ throw new Error(
7748
+ `sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
7749
+ );
7750
+ }
7751
+ }
7752
+ } catch (error) {
7753
+ throwSandboxOperationError("sandbox writeFiles", error, true);
7754
+ }
7755
+ }
7756
+ );
7410
7757
  }
7411
- seen.add(current);
7412
- current = typeof current === "object" ? current.cause : void 0;
7413
- }
7414
- return void 0;
7758
+ );
7415
7759
  }
7416
- function wrapSandboxSetupError(error) {
7417
- try {
7418
- const details = getSandboxErrorDetails(error);
7419
- if (details.summary) {
7420
- return new Error(`sandbox setup failed (${details.summary})`, {
7421
- cause: error
7422
- });
7423
- }
7424
- } catch {
7425
- }
7426
- let causeMessage;
7427
- try {
7428
- causeMessage = getFirstErrorMessage(error);
7429
- } catch (cause) {
7430
- causeMessage = cause instanceof Error ? cause.message : void 0;
7760
+
7761
+ // src/chat/sandbox/session.ts
7762
+ var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
7763
+ var SANDBOX_RUNTIME = "node22";
7764
+ var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
7765
+ var SNAPSHOT_BOOT_RETRY_COUNT = 3;
7766
+ var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
7767
+ var SNAPSHOT_PHASE_STATUS = {
7768
+ resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
7769
+ waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
7770
+ building_snapshot: { kind: "creating", context: "sandbox snapshot" },
7771
+ cache_hit: { kind: "loading", context: "sandbox snapshot" }
7772
+ };
7773
+ function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
7774
+ const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
7775
+ const existingAllowRaw = basePolicy.allow;
7776
+ const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
7777
+ Object.entries(existingAllowRaw).map(
7778
+ ([domain, rules]) => [
7779
+ domain,
7780
+ Array.isArray(rules) ? [...rules] : []
7781
+ ]
7782
+ )
7783
+ ) : { "*": [] };
7784
+ for (const transform of headerTransforms) {
7785
+ const currentRules = existingAllow[transform.domain] ?? [];
7786
+ existingAllow[transform.domain] = [
7787
+ ...currentRules,
7788
+ { transform: [{ headers: transform.headers }] }
7789
+ ];
7431
7790
  }
7432
- if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7433
- const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7434
- return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7791
+ return {
7792
+ ...basePolicy,
7793
+ allow: existingAllow
7794
+ };
7795
+ }
7796
+ function truncateOutput(output, maxLength) {
7797
+ if (output.length <= maxLength) {
7798
+ return { value: output, truncated: false };
7435
7799
  }
7436
- return new Error("sandbox setup failed", { cause: error });
7800
+ const truncatedLength = output.length - maxLength;
7801
+ return {
7802
+ value: `${output.slice(0, maxLength)}
7803
+
7804
+ [output truncated: ${truncatedLength} characters removed]`,
7805
+ truncated: true
7806
+ };
7437
7807
  }
7438
- function throwSandboxOperationError(action, error, includeMissingPath = false) {
7439
- const details = getSandboxErrorDetails(error);
7440
- setSpanAttributes({
7441
- ...details.attributes,
7442
- ...includeMissingPath ? {
7443
- "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7444
- } : {},
7445
- "app.sandbox.success": false
7808
+ function sleep2(ms) {
7809
+ return new Promise((resolve) => {
7810
+ setTimeout(resolve, ms);
7446
7811
  });
7447
- setSpanStatus("error");
7448
- throw new Error(
7449
- details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7450
- {
7451
- cause: error
7812
+ }
7813
+ function createStatusEmitter(emitStatus) {
7814
+ let statusCount = 0;
7815
+ const sentStatuses = /* @__PURE__ */ new Set();
7816
+ const emit = async (status) => {
7817
+ const statusKey = `${status.kind}:${status.context ?? ""}`;
7818
+ if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
7819
+ return;
7820
+ }
7821
+ sentStatuses.add(statusKey);
7822
+ statusCount += 1;
7823
+ await emitStatus(status);
7824
+ };
7825
+ const reportSnapshotPhase = async (phase) => {
7826
+ const status = SNAPSHOT_PHASE_STATUS[phase];
7827
+ if (status) {
7828
+ await emit(makeAssistantStatus(status.kind, status.context));
7452
7829
  }
7830
+ };
7831
+ return { emit, reportSnapshotPhase };
7832
+ }
7833
+ function parseKeepAliveMs() {
7834
+ const parsed = Number.parseInt(
7835
+ process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
7836
+ 10
7453
7837
  );
7838
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
7454
7839
  }
7455
- function createSandboxExecutor(options) {
7840
+ function createSandboxSessionManager(options) {
7456
7841
  let sandbox = null;
7457
7842
  let sandboxIdHint = options?.sandboxId;
7458
7843
  let availableSkills = [];
7459
7844
  let toolExecutors;
7460
7845
  const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
7461
7846
  const traceContext = options?.traceContext ?? {};
7462
- const emitStatus = options?.onStatus;
7463
7847
  const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
7464
7848
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
7465
- const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, onStatus) => {
7466
- for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
7467
- try {
7468
- await onStatus?.("Booting up...");
7469
- return await Sandbox.create({
7470
- timeout: timeoutMs,
7471
- source: {
7472
- type: "snapshot",
7473
- snapshotId
7474
- },
7475
- ...sandboxCredentials ?? {}
7476
- });
7477
- } catch (error) {
7478
- if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
7479
- throw error;
7849
+ const emitSandboxStatus = async (source, statusEmitter, status) => {
7850
+ logInfo(
7851
+ "sandbox_status_emitted",
7852
+ traceContext,
7853
+ {
7854
+ "app.sandbox.status.source": source,
7855
+ "app.sandbox.status.kind": status.kind,
7856
+ ...status.context ? { "app.sandbox.status.context": status.context } : {}
7857
+ },
7858
+ "Sandbox status emitted"
7859
+ );
7860
+ if (typeof statusEmitter === "function") {
7861
+ await statusEmitter(status);
7862
+ return;
7863
+ }
7864
+ await statusEmitter.emit(status);
7865
+ };
7866
+ const clearSession = () => {
7867
+ sandbox = null;
7868
+ sandboxIdHint = void 0;
7869
+ toolExecutors = void 0;
7870
+ };
7871
+ const rememberSandbox = (nextSandbox) => {
7872
+ sandbox = nextSandbox;
7873
+ sandboxIdHint = nextSandbox.sandboxId;
7874
+ toolExecutors = void 0;
7875
+ return nextSandbox;
7876
+ };
7877
+ const failSetup = (error) => {
7878
+ throw wrapSandboxSetupError(error);
7879
+ };
7880
+ const syncSkills = async (targetSandbox) => {
7881
+ await syncSkillsToSandbox({
7882
+ sandbox: targetSandbox,
7883
+ skills: availableSkills,
7884
+ withSpan: withSandboxSpan,
7885
+ runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
7886
+ });
7887
+ };
7888
+ const ensureSandboxReachable = async (targetSandbox, source) => {
7889
+ await withSandboxSpan(
7890
+ "sandbox.reuse_probe",
7891
+ "sandbox.acquire.probe",
7892
+ {
7893
+ "app.sandbox.reused": true,
7894
+ "app.sandbox.source": source
7895
+ },
7896
+ async () => {
7897
+ try {
7898
+ await targetSandbox.mkDir(SANDBOX_WORKSPACE_ROOT);
7899
+ } catch (error) {
7900
+ if (!isAlreadyExistsError(error)) {
7901
+ throw error;
7902
+ }
7480
7903
  }
7481
- await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
7482
7904
  }
7483
- }
7484
- throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
7905
+ );
7485
7906
  };
7486
7907
  const invalidateSandboxInstance = async (targetSandbox, reason) => {
7487
7908
  if (sandbox === targetSandbox) {
7488
- sandbox = null;
7489
- sandboxIdHint = void 0;
7490
- toolExecutors = void 0;
7909
+ clearSession();
7491
7910
  }
7492
7911
  logWarn(
7493
7912
  "sandbox_network_policy_restore_failed",
@@ -7502,277 +7921,304 @@ function createSandboxExecutor(options) {
7502
7921
  } catch {
7503
7922
  }
7504
7923
  };
7505
- const upsertSkillsToSandbox = async (targetSandbox) => {
7506
- await withSandboxSpan(
7507
- "sandbox.sync_skills",
7508
- "sandbox.sync",
7509
- {
7510
- "app.sandbox.skills_count": availableSkills.length
7511
- },
7512
- async () => {
7513
- const filesToWrite = await buildSkillSyncFiles(availableSkills);
7514
- const bytesWritten = filesToWrite.reduce(
7515
- (total, file) => total + file.content.length,
7516
- 0
7517
- );
7518
- const directories = collectDirectories(filesToWrite);
7519
- await withSandboxSpan(
7520
- "sandbox.sync_writeFiles",
7521
- "sandbox.sync.write",
7522
- {
7523
- "app.sandbox.sync.files_written": filesToWrite.length,
7524
- "app.sandbox.sync.bytes_written": bytesWritten,
7525
- "app.sandbox.sync.directories_ensured": directories.length
7526
- },
7527
- async () => {
7528
- try {
7529
- for (const directory of directories) {
7530
- try {
7531
- await targetSandbox.mkDir(directory);
7532
- } catch (error) {
7533
- if (!isAlreadyExistsError(error)) {
7534
- throw error;
7535
- }
7536
- }
7537
- }
7538
- await targetSandbox.writeFiles(filesToWrite);
7539
- const executableFiles = filesToWrite.map((file) => file.path).filter(
7540
- (filePath) => filePath.startsWith(`${SANDBOX_RUNTIME_BIN_DIR}/`)
7541
- );
7542
- for (const filePath of executableFiles) {
7543
- const chmod = await runNonInteractiveCommand(targetSandbox, {
7544
- cmd: "chmod",
7545
- args: ["0755", filePath],
7546
- cwd: SANDBOX_WORKSPACE_ROOT
7547
- });
7548
- if (chmod.exitCode !== 0) {
7549
- throw new Error(
7550
- `sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
7551
- );
7552
- }
7553
- }
7554
- } catch (error) {
7555
- throwSandboxOperationError("sandbox writeFiles", error, true);
7556
- }
7557
- }
7558
- );
7924
+ const recreateUnavailableSandbox = async (source) => {
7925
+ setSpanAttributes({
7926
+ "app.sandbox.recovery.attempted": true,
7927
+ "app.sandbox.recovery.source": source
7928
+ });
7929
+ clearSession();
7930
+ const replacement = await createFreshSandbox();
7931
+ setSpanAttributes({
7932
+ "app.sandbox.recovery.succeeded": true
7933
+ });
7934
+ return replacement;
7935
+ };
7936
+ const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
7937
+ for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
7938
+ try {
7939
+ if (emitStatus) {
7940
+ await emitSandboxStatus(
7941
+ "snapshot_boot",
7942
+ emitStatus,
7943
+ makeAssistantStatus("loading", "sandbox")
7944
+ );
7945
+ }
7946
+ return await Sandbox.create({
7947
+ timeout: timeoutMs,
7948
+ source: {
7949
+ type: "snapshot",
7950
+ snapshotId
7951
+ },
7952
+ ...sandboxCredentials ?? {}
7953
+ });
7954
+ } catch (error) {
7955
+ if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
7956
+ throw error;
7957
+ }
7958
+ await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
7559
7959
  }
7560
- );
7960
+ }
7961
+ throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
7962
+ };
7963
+ const setSnapshotAttributes = (snapshot) => {
7964
+ setSpanAttributes({
7965
+ "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
7966
+ "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
7967
+ "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
7968
+ ...snapshot.profileHash ? {
7969
+ "app.sandbox.snapshot.profile_hash": snapshot.profileHash
7970
+ } : {},
7971
+ "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
7972
+ ...snapshot.rebuildReason ? {
7973
+ "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
7974
+ } : {}
7975
+ });
7976
+ };
7977
+ const createSandboxFromResolvedSnapshot = async (params) => {
7978
+ const { runtime, snapshot, sandboxCredentials, status } = params;
7979
+ if (!snapshot.snapshotId) {
7980
+ await emitSandboxStatus(
7981
+ "fresh_runtime_boot",
7982
+ status,
7983
+ makeAssistantStatus("loading", "sandbox")
7984
+ );
7985
+ return await Sandbox.create({
7986
+ timeout: timeoutMs,
7987
+ runtime,
7988
+ ...sandboxCredentials ?? {}
7989
+ });
7990
+ }
7991
+ try {
7992
+ return await createSandboxFromSnapshot(
7993
+ snapshot.snapshotId,
7994
+ sandboxCredentials,
7995
+ status.emit
7996
+ );
7997
+ } catch (error) {
7998
+ if (!isSnapshotMissingError(error)) {
7999
+ throw error;
8000
+ }
8001
+ setSpanAttributes({
8002
+ "app.sandbox.snapshot.rebuild_after_missing": true
8003
+ });
8004
+ const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
8005
+ runtime,
8006
+ timeoutMs,
8007
+ forceRebuild: true,
8008
+ staleSnapshotId: snapshot.snapshotId,
8009
+ onProgress: status.reportSnapshotPhase
8010
+ });
8011
+ if (!rebuiltSnapshot.snapshotId) {
8012
+ throw error;
8013
+ }
8014
+ return await createSandboxFromSnapshot(
8015
+ rebuiltSnapshot.snapshotId,
8016
+ sandboxCredentials,
8017
+ status.emit
8018
+ );
8019
+ }
8020
+ };
8021
+ const createFreshSandbox = async () => {
8022
+ const runtime = SANDBOX_RUNTIME;
8023
+ const sandboxCredentials = getVercelSandboxCredentials();
8024
+ const status = createStatusEmitter(options?.onStatus);
8025
+ let createdSandbox;
8026
+ try {
8027
+ createdSandbox = await withSandboxSpan(
8028
+ "sandbox.create",
8029
+ "sandbox.create",
8030
+ {
8031
+ "app.sandbox.reused": false,
8032
+ "app.sandbox.timeout_ms": timeoutMs,
8033
+ "app.sandbox.runtime": runtime
8034
+ },
8035
+ async () => {
8036
+ await emitSandboxStatus(
8037
+ "runtime_dependency_resolve",
8038
+ status,
8039
+ makeAssistantStatus("loading", "sandbox runtime")
8040
+ );
8041
+ const snapshot = await resolveRuntimeDependencySnapshot({
8042
+ runtime,
8043
+ timeoutMs,
8044
+ onProgress: status.reportSnapshotPhase
8045
+ });
8046
+ setSnapshotAttributes(snapshot);
8047
+ return await createSandboxFromResolvedSnapshot({
8048
+ runtime,
8049
+ snapshot,
8050
+ sandboxCredentials,
8051
+ status
8052
+ });
8053
+ }
8054
+ );
8055
+ } catch (error) {
8056
+ return failSetup(error);
8057
+ }
8058
+ try {
8059
+ await syncSkills(createdSandbox);
8060
+ } catch (error) {
8061
+ return failSetup(error);
8062
+ }
8063
+ return rememberSandbox(createdSandbox);
8064
+ };
8065
+ const discardHintIfProfileChanged = () => {
8066
+ if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
8067
+ return;
8068
+ }
8069
+ setSpanAttributes({
8070
+ "app.sandbox.reused": false,
8071
+ "app.sandbox.recreate.reason": "dependency_profile_mismatch",
8072
+ ...options?.sandboxDependencyProfileHash ? {
8073
+ "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
8074
+ } : {},
8075
+ ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8076
+ });
8077
+ sandboxIdHint = void 0;
8078
+ };
8079
+ const tryReuseCachedSandbox = async () => {
8080
+ const cachedSandbox = sandbox;
8081
+ if (!cachedSandbox) {
8082
+ return null;
8083
+ }
8084
+ try {
8085
+ await ensureSandboxReachable(cachedSandbox, "memory");
8086
+ return cachedSandbox;
8087
+ } catch (error) {
8088
+ if (isSandboxUnavailableError(error)) {
8089
+ return await recreateUnavailableSandbox("memory");
8090
+ }
8091
+ return failSetup(error);
8092
+ }
8093
+ };
8094
+ const tryRestoreHintedSandbox = async () => {
8095
+ if (!sandboxIdHint) {
8096
+ return null;
8097
+ }
8098
+ let hintedSandbox = null;
8099
+ try {
8100
+ const sandboxCredentials = getVercelSandboxCredentials();
8101
+ hintedSandbox = await withSandboxSpan(
8102
+ "sandbox.get",
8103
+ "sandbox.get",
8104
+ {
8105
+ "app.sandbox.reused": true,
8106
+ "app.sandbox.source": "id_hint"
8107
+ },
8108
+ async () => await Sandbox.get({
8109
+ sandboxId: sandboxIdHint,
8110
+ ...sandboxCredentials ?? {}
8111
+ })
8112
+ );
8113
+ } catch {
8114
+ return null;
8115
+ }
8116
+ try {
8117
+ await syncSkills(hintedSandbox);
8118
+ return rememberSandbox(hintedSandbox);
8119
+ } catch (error) {
8120
+ if (isSandboxUnavailableError(error)) {
8121
+ return await recreateUnavailableSandbox("id_hint");
8122
+ }
8123
+ return failSetup(error);
8124
+ }
7561
8125
  };
7562
8126
  const acquireSandbox = async () => {
7563
- return withSandboxSpan(
8127
+ return await withSandboxSpan(
7564
8128
  "sandbox.acquire",
7565
8129
  "sandbox.acquire",
7566
8130
  {
7567
8131
  "app.sandbox.id_hint_present": Boolean(sandboxIdHint),
7568
8132
  "app.sandbox.timeout_ms": timeoutMs,
7569
- "app.sandbox.runtime": "node22",
8133
+ "app.sandbox.runtime": SANDBOX_RUNTIME,
7570
8134
  "app.sandbox.skills_count": availableSkills.length
7571
8135
  },
7572
8136
  async () => {
7573
- const sandboxCredentials = getVercelSandboxCredentials();
7574
- const assignSandbox = (nextSandbox) => {
7575
- sandbox = nextSandbox;
7576
- sandboxIdHint = nextSandbox.sandboxId;
7577
- toolExecutors = void 0;
7578
- return nextSandbox;
7579
- };
7580
- const handleSetupFailure = (error) => {
7581
- throw wrapSandboxSetupError(error);
7582
- };
7583
- const createFreshSandbox = async () => {
7584
- const runtime = SANDBOX_RUNTIME;
7585
- let statusCount = 0;
7586
- const sentStatuses = /* @__PURE__ */ new Set();
7587
- const emitSandboxStatus = async (status) => {
7588
- if (!emitStatus || statusCount >= 4 || sentStatuses.has(status)) {
7589
- return;
7590
- }
7591
- sentStatuses.add(status);
7592
- statusCount += 1;
7593
- await emitStatus(status);
7594
- };
7595
- const reportSnapshotPhase = async (phase) => {
7596
- if (phase === "resolve_start") {
7597
- await emitSandboxStatus("Checking sandbox snapshot cache...");
7598
- return;
7599
- }
7600
- if (phase === "waiting_for_lock") {
7601
- await emitSandboxStatus("Waiting for sandbox snapshot build...");
7602
- return;
7603
- }
7604
- if (phase === "building_snapshot") {
7605
- await emitSandboxStatus("Building sandbox snapshot...");
7606
- return;
7607
- }
7608
- if (phase === "cache_hit") {
7609
- await emitSandboxStatus("Using cached sandbox snapshot...");
7610
- }
7611
- };
7612
- let createdSandbox;
7613
- try {
7614
- createdSandbox = await withSandboxSpan(
7615
- "sandbox.create",
7616
- "sandbox.create",
7617
- {
7618
- "app.sandbox.reused": false,
7619
- "app.sandbox.timeout_ms": timeoutMs,
7620
- "app.sandbox.runtime": runtime
7621
- },
7622
- async () => {
7623
- await emitSandboxStatus("Preparing sandbox runtime...");
7624
- const snapshot = await resolveRuntimeDependencySnapshot({
7625
- runtime,
7626
- timeoutMs,
7627
- onProgress: reportSnapshotPhase
7628
- });
7629
- setSpanAttributes({
7630
- "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
7631
- "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
7632
- "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
7633
- ...snapshot.profileHash ? {
7634
- "app.sandbox.snapshot.profile_hash": snapshot.profileHash
7635
- } : {},
7636
- "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
7637
- ...snapshot.rebuildReason ? {
7638
- "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
7639
- } : {}
7640
- });
7641
- if (!snapshot.snapshotId) {
7642
- await emitSandboxStatus("Booting up...");
7643
- return await Sandbox.create({
7644
- timeout: timeoutMs,
7645
- runtime,
7646
- ...sandboxCredentials ?? {}
7647
- });
7648
- }
7649
- try {
7650
- return await createSandboxFromSnapshot(
7651
- snapshot.snapshotId,
7652
- sandboxCredentials,
7653
- emitSandboxStatus
7654
- );
7655
- } catch (error) {
7656
- if (!isSnapshotMissingError(error)) {
7657
- throw error;
7658
- }
7659
- setSpanAttributes({
7660
- "app.sandbox.snapshot.rebuild_after_missing": true
7661
- });
7662
- const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
7663
- runtime,
7664
- timeoutMs,
7665
- forceRebuild: true,
7666
- staleSnapshotId: snapshot.snapshotId,
7667
- onProgress: reportSnapshotPhase
7668
- });
7669
- if (!rebuiltSnapshot.snapshotId) {
7670
- throw error;
7671
- }
7672
- return await createSandboxFromSnapshot(
7673
- rebuiltSnapshot.snapshotId,
7674
- sandboxCredentials,
7675
- emitSandboxStatus
7676
- );
7677
- }
7678
- }
7679
- );
7680
- } catch (error) {
7681
- return handleSetupFailure(error);
7682
- }
7683
- try {
7684
- await upsertSkillsToSandbox(createdSandbox);
7685
- } catch (error) {
7686
- return handleSetupFailure(error);
7687
- }
7688
- return assignSandbox(createdSandbox);
7689
- };
7690
- if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
7691
- setSpanAttributes({
7692
- "app.sandbox.reused": false,
7693
- "app.sandbox.recreate.reason": "dependency_profile_mismatch",
7694
- ...options?.sandboxDependencyProfileHash ? {
7695
- "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
7696
- } : {},
7697
- ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
7698
- });
7699
- sandboxIdHint = void 0;
8137
+ discardHintIfProfileChanged();
8138
+ const cachedSandbox = await tryReuseCachedSandbox();
8139
+ if (cachedSandbox) {
8140
+ return cachedSandbox;
7700
8141
  }
7701
- const recoverUnavailableSandbox = async (source) => {
7702
- setSpanAttributes({
7703
- "app.sandbox.recovery.attempted": true,
7704
- "app.sandbox.recovery.source": source
7705
- });
7706
- sandbox = null;
7707
- sandboxIdHint = void 0;
7708
- toolExecutors = void 0;
7709
- const replacement = await createFreshSandbox();
7710
- setSpanAttributes({
7711
- "app.sandbox.recovery.succeeded": true
7712
- });
7713
- return replacement;
7714
- };
7715
- if (sandbox) {
7716
- const cachedSandbox = sandbox;
7717
- try {
7718
- await withSandboxSpan(
7719
- "sandbox.reuse_cached",
7720
- "sandbox.acquire.cached",
7721
- {
7722
- "app.sandbox.reused": true,
7723
- "app.sandbox.source": "memory"
7724
- },
7725
- async () => {
7726
- await upsertSkillsToSandbox(cachedSandbox);
7727
- }
7728
- );
7729
- return cachedSandbox;
7730
- } catch (error) {
7731
- if (isSandboxUnavailableError(error)) {
7732
- return recoverUnavailableSandbox("memory");
7733
- }
7734
- return handleSetupFailure(error);
7735
- }
8142
+ const hintedSandbox = await tryRestoreHintedSandbox();
8143
+ if (hintedSandbox) {
8144
+ return hintedSandbox;
7736
8145
  }
7737
- let acquiredSandbox = null;
7738
- if (sandboxIdHint) {
7739
- try {
7740
- acquiredSandbox = await withSandboxSpan(
7741
- "sandbox.get",
7742
- "sandbox.get",
7743
- {
7744
- "app.sandbox.reused": true,
7745
- "app.sandbox.source": "id_hint"
7746
- },
7747
- async () => Sandbox.get({
7748
- sandboxId: sandboxIdHint,
7749
- ...sandboxCredentials ?? {}
7750
- })
7751
- );
7752
- } catch {
7753
- acquiredSandbox = null;
7754
- }
7755
- }
7756
- if (acquiredSandbox) {
7757
- try {
7758
- await upsertSkillsToSandbox(acquiredSandbox);
7759
- return assignSandbox(acquiredSandbox);
7760
- } catch (error) {
7761
- if (isSandboxUnavailableError(error)) {
7762
- return recoverUnavailableSandbox("id_hint");
7763
- }
7764
- return handleSetupFailure(error);
7765
- }
7766
- }
7767
- return createFreshSandbox();
8146
+ return await createFreshSandbox();
7768
8147
  }
7769
8148
  );
7770
8149
  };
7771
- const getToolExecutors = async () => {
7772
- if (toolExecutors) {
7773
- return toolExecutors;
8150
+ const getMaxOutputLength = () => {
8151
+ const maxOutputLength = Number.parseInt(
8152
+ process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
8153
+ 10
8154
+ );
8155
+ return Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
8156
+ };
8157
+ const readCommandOutput = async (commandResult2) => {
8158
+ const boundedOutputLength = getMaxOutputLength();
8159
+ const stdoutRaw = await commandResult2.stdout();
8160
+ const stderrRaw = await commandResult2.stderr();
8161
+ const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
8162
+ const stderr = truncateOutput(stderrRaw, boundedOutputLength);
8163
+ return {
8164
+ stdout: stdout.value,
8165
+ stderr: stderr.value,
8166
+ exitCode: commandResult2.exitCode,
8167
+ stdoutTruncated: stdout.truncated,
8168
+ stderrTruncated: stderr.truncated
8169
+ };
8170
+ };
8171
+ const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
8172
+ if (!headerTransforms || headerTransforms.length === 0) {
8173
+ return await callback();
8174
+ }
8175
+ const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
8176
+ const policy = mergeNetworkPolicyWithHeaderTransforms(
8177
+ restoreNetworkPolicy,
8178
+ headerTransforms
8179
+ );
8180
+ await sandboxInstance.updateNetworkPolicy(policy);
8181
+ let callbackError;
8182
+ let restoreError;
8183
+ let result;
8184
+ try {
8185
+ result = await callback();
8186
+ } catch (error) {
8187
+ callbackError = error;
8188
+ throw error;
8189
+ } finally {
8190
+ try {
8191
+ await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
8192
+ } catch (error) {
8193
+ restoreError = error;
8194
+ await invalidateSandboxInstance(sandboxInstance, error);
8195
+ }
7774
8196
  }
7775
- const activeSandbox = await acquireSandbox();
8197
+ if (restoreError && !callbackError) {
8198
+ throw restoreError;
8199
+ }
8200
+ return result;
8201
+ };
8202
+ const extendKeepAlive = async (activeSandbox) => {
8203
+ const keepAliveMs = parseKeepAliveMs();
8204
+ if (keepAliveMs === 0) {
8205
+ return;
8206
+ }
8207
+ try {
8208
+ await withSandboxSpan(
8209
+ "sandbox.keepalive.extend",
8210
+ "sandbox.keepalive",
8211
+ {
8212
+ "app.sandbox.keepalive_ms": keepAliveMs
8213
+ },
8214
+ async () => {
8215
+ await activeSandbox.extendTimeout(keepAliveMs);
8216
+ }
8217
+ );
8218
+ } catch {
8219
+ }
8220
+ };
8221
+ const buildToolExecutors = async (sandboxInstance) => {
7776
8222
  const toolkit = await withSandboxSpan(
7777
8223
  "sandbox.bash_tool.init",
7778
8224
  "sandbox.tool.init",
@@ -7780,8 +8226,8 @@ function createSandboxExecutor(options) {
7780
8226
  "app.sandbox.tool_name": "bash",
7781
8227
  "app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
7782
8228
  },
7783
- async () => createBashTool2({
7784
- sandbox: activeSandbox,
8229
+ async () => await createBashTool2({
8230
+ sandbox: sandboxInstance,
7785
8231
  destination: SANDBOX_WORKSPACE_ROOT
7786
8232
  })
7787
8233
  );
@@ -7790,63 +8236,24 @@ function createSandboxExecutor(options) {
7790
8236
  if (!executeReadFile || !executeWriteFile) {
7791
8237
  throw new Error("bash-tool did not return executable tool handlers");
7792
8238
  }
7793
- toolExecutors = {
8239
+ return {
7794
8240
  bash: async (input) => {
7795
- const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
7796
- const headerTransforms = input.headerTransforms;
7797
- if (headerTransforms && headerTransforms.length > 0) {
7798
- const policy = mergeNetworkPolicyWithHeaderTransforms(
7799
- restoreNetworkPolicy,
7800
- headerTransforms
7801
- );
7802
- await activeSandbox.updateNetworkPolicy(policy);
7803
- }
7804
8241
  const script = buildNonInteractiveShellScript(input.command, {
7805
8242
  env: input.env,
7806
8243
  pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
7807
8244
  });
7808
- let commandError;
7809
- let result;
7810
- let restoreError;
7811
- try {
7812
- const commandResult2 = await activeSandbox.runCommand({
7813
- cmd: "bash",
7814
- args: ["-c", script],
7815
- cwd: SANDBOX_WORKSPACE_ROOT
7816
- });
7817
- const maxOutputLength = Number.parseInt(
7818
- process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
7819
- 10
7820
- );
7821
- const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
7822
- const stdoutRaw = await commandResult2.stdout();
7823
- const stderrRaw = await commandResult2.stderr();
7824
- const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
7825
- const stderr = truncateOutput(stderrRaw, boundedOutputLength);
7826
- result = {
7827
- stdout: stdout.value,
7828
- stderr: stderr.value,
7829
- exitCode: commandResult2.exitCode,
7830
- stdoutTruncated: stdout.truncated,
7831
- stderrTruncated: stderr.truncated
7832
- };
7833
- } catch (error) {
7834
- commandError = error;
7835
- throw error;
7836
- } finally {
7837
- if (headerTransforms && headerTransforms.length > 0) {
7838
- try {
7839
- await activeSandbox.updateNetworkPolicy(restoreNetworkPolicy);
7840
- } catch (error) {
7841
- restoreError = error;
7842
- await invalidateSandboxInstance(activeSandbox, error);
7843
- }
8245
+ return await withTemporaryHeaderTransforms(
8246
+ sandboxInstance,
8247
+ input.headerTransforms,
8248
+ async () => {
8249
+ const commandResult2 = await sandboxInstance.runCommand({
8250
+ cmd: "bash",
8251
+ args: ["-c", script],
8252
+ cwd: SANDBOX_WORKSPACE_ROOT
8253
+ });
8254
+ return await readCommandOutput(commandResult2);
7844
8255
  }
7845
- }
7846
- if (restoreError && !commandError) {
7847
- throw restoreError;
7848
- }
7849
- return result;
8256
+ );
7850
8257
  },
7851
8258
  readFile: async (input) => await executeReadFile(input, {
7852
8259
  toolCallId: "sandbox-read-file",
@@ -7857,191 +8264,18 @@ function createSandboxExecutor(options) {
7857
8264
  messages: []
7858
8265
  })
7859
8266
  };
7860
- return toolExecutors;
7861
8267
  };
7862
- const execute = async (params) => {
7863
- const rawInput = params.input ?? {};
7864
- const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
7865
- if (params.toolName === "bash") {
7866
- if (!bashCommand) {
7867
- throw new Error("command is required");
7868
- }
7869
- if (options?.runBashCustomCommand) {
7870
- const custom = await options.runBashCustomCommand(bashCommand);
7871
- if (custom.handled) {
7872
- return { result: custom.result };
7873
- }
7874
- }
7875
- }
8268
+ const ensureReadySandbox = async () => {
7876
8269
  const activeSandbox = await acquireSandbox();
7877
- const keepAliveMs = Number.parseInt(
7878
- process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
7879
- 10
7880
- );
7881
- if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
7882
- try {
7883
- await withSandboxSpan(
7884
- "sandbox.keepalive.extend",
7885
- "sandbox.keepalive",
7886
- {
7887
- "app.sandbox.keepalive_ms": keepAliveMs
7888
- },
7889
- async () => {
7890
- await activeSandbox.extendTimeout(keepAliveMs);
7891
- }
7892
- );
7893
- } catch {
7894
- }
7895
- }
7896
- if (params.toolName === "bash") {
7897
- const command = bashCommand;
7898
- const headerTransformsInput = rawInput.headerTransforms;
7899
- const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
7900
- (value) => Boolean(value && typeof value === "object")
7901
- ).map((transform) => ({
7902
- domain: String(transform.domain ?? "").trim(),
7903
- headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
7904
- Object.entries(
7905
- transform.headers
7906
- ).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
7907
- ) : {}
7908
- })).filter(
7909
- (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
7910
- ) : void 0;
7911
- const envInput = rawInput.env;
7912
- const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
7913
- Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
7914
- ) : void 0;
7915
- const executeBash = (await getToolExecutors()).bash;
7916
- const result = await withSandboxSpan(
7917
- "bash",
7918
- "process.exec",
7919
- {
7920
- "process.executable.name": "bash"
7921
- },
7922
- async () => {
7923
- try {
7924
- const response = await executeBash({
7925
- command,
7926
- ...headerTransforms ? { headerTransforms } : {},
7927
- ...env ? { env } : {}
7928
- });
7929
- setSpanAttributes({
7930
- "process.exit.code": response.exitCode,
7931
- "app.sandbox.stdout_bytes": Buffer.byteLength(
7932
- response.stdout ?? "",
7933
- "utf8"
7934
- ),
7935
- "app.sandbox.stderr_bytes": Buffer.byteLength(
7936
- response.stderr ?? "",
7937
- "utf8"
7938
- ),
7939
- ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
7940
- });
7941
- setSpanStatus(response.exitCode === 0 ? "ok" : "error");
7942
- return response;
7943
- } catch (error) {
7944
- setSpanAttributes({
7945
- "error.type": error instanceof Error ? error.name : "sandbox_execute_error"
7946
- });
7947
- setSpanStatus("error");
7948
- throw error;
7949
- }
7950
- }
7951
- );
7952
- return {
7953
- result: {
7954
- ok: result.exitCode === 0,
7955
- command,
7956
- cwd: SANDBOX_WORKSPACE_ROOT,
7957
- exit_code: result.exitCode,
7958
- signal: null,
7959
- timed_out: false,
7960
- stdout: result.stdout,
7961
- stderr: result.stderr,
7962
- stdout_truncated: result.stdoutTruncated,
7963
- stderr_truncated: result.stderrTruncated
7964
- }
7965
- };
7966
- }
7967
- if (params.toolName === "readFile") {
7968
- const filePath = String(rawInput.path ?? "").trim();
7969
- if (!filePath) {
7970
- throw new Error("path is required");
7971
- }
7972
- const executeReadFile = (await getToolExecutors()).readFile;
7973
- const result = await withSandboxSpan(
7974
- "sandbox.readFile",
7975
- "sandbox.fs.read",
7976
- {
7977
- "app.sandbox.path.length": filePath.length
7978
- },
7979
- async () => {
7980
- const response = await executeReadFile({ path: filePath });
7981
- const content = String(response.content ?? "");
7982
- setSpanAttributes({
7983
- "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
7984
- "app.sandbox.read.chars": content.length
7985
- });
7986
- setSpanStatus("ok");
7987
- return {
7988
- content,
7989
- path: filePath,
7990
- success: true
7991
- };
7992
- }
7993
- );
7994
- return { result };
7995
- }
7996
- if (params.toolName === "writeFile") {
7997
- const filePath = String(rawInput.path ?? "").trim();
7998
- if (!filePath) {
7999
- throw new Error("path is required");
8000
- }
8001
- const content = String(rawInput.content ?? "");
8002
- const executeWriteFile = (await getToolExecutors()).writeFile;
8003
- await withSandboxSpan(
8004
- "sandbox.writeFile",
8005
- "sandbox.fs.write",
8006
- {
8007
- "app.sandbox.path.length": filePath.length,
8008
- "app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
8009
- },
8010
- async () => {
8011
- try {
8012
- await executeWriteFile({ path: filePath, content });
8013
- setSpanStatus("ok");
8014
- } catch (error) {
8015
- throwSandboxOperationError("sandbox writeFile", error);
8016
- }
8017
- }
8018
- );
8019
- return {
8020
- result: {
8021
- ok: true,
8022
- path: filePath,
8023
- bytes_written: Buffer.byteLength(content, "utf8")
8024
- }
8025
- };
8026
- }
8027
- throw new Error(`unsupported sandbox tool: ${params.toolName}`);
8270
+ await extendKeepAlive(activeSandbox);
8271
+ return activeSandbox;
8028
8272
  };
8029
- const dispose = async () => {
8030
- if (!sandbox) {
8031
- return;
8273
+ const loadToolExecutors = async (activeSandbox) => {
8274
+ if (toolExecutors) {
8275
+ return toolExecutors;
8032
8276
  }
8033
- await withSandboxSpan(
8034
- "sandbox.stop",
8035
- "sandbox.stop",
8036
- {
8037
- "app.sandbox.stop.blocking": true
8038
- },
8039
- async () => {
8040
- await sandbox.stop({ blocking: true });
8041
- }
8042
- );
8043
- sandbox = null;
8044
- toolExecutors = void 0;
8277
+ toolExecutors = await buildToolExecutors(activeSandbox);
8278
+ return toolExecutors;
8045
8279
  };
8046
8280
  return {
8047
8281
  configureSkills(skills) {
@@ -8053,199 +8287,298 @@ function createSandboxExecutor(options) {
8053
8287
  getDependencyProfileHash() {
8054
8288
  return dependencyProfileHash;
8055
8289
  },
8056
- canExecute(toolName) {
8057
- return SANDBOX_TOOL_NAMES.has(toolName);
8058
- },
8059
8290
  async createSandbox() {
8060
8291
  return await acquireSandbox();
8061
8292
  },
8062
- execute,
8063
- dispose
8293
+ async ensureToolExecutors() {
8294
+ return await loadToolExecutors(await ensureReadySandbox());
8295
+ },
8296
+ async dispose() {
8297
+ const activeSandbox = sandbox;
8298
+ if (!activeSandbox) {
8299
+ return;
8300
+ }
8301
+ await withSandboxSpan(
8302
+ "sandbox.stop",
8303
+ "sandbox.stop",
8304
+ {
8305
+ "app.sandbox.stop.blocking": true
8306
+ },
8307
+ async () => {
8308
+ await activeSandbox.stop({ blocking: true });
8309
+ }
8310
+ );
8311
+ sandbox = null;
8312
+ toolExecutors = void 0;
8313
+ }
8064
8314
  };
8065
8315
  }
8066
8316
 
8067
- // src/chat/runtime/dev-agent-trace.ts
8068
- function shouldEmitDevAgentTrace() {
8069
- return process.env.NODE_ENV === "development";
8070
- }
8071
-
8072
- // src/chat/runtime/status-format.ts
8073
- var SLACK_STATUS_MAX_LENGTH = 50;
8074
- function truncateWithEllipsis(text, maxLength) {
8075
- if (text.length <= maxLength) {
8076
- return text;
8077
- }
8078
- return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
8079
- }
8080
- function truncateStatusText(text) {
8081
- const trimmed = text.trim();
8082
- if (!trimmed) {
8083
- return "";
8084
- }
8085
- return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
8086
- }
8087
- function compactStatusPath(value) {
8088
- if (typeof value !== "string") {
8089
- return void 0;
8090
- }
8091
- const trimmed = value.trim();
8092
- if (!trimmed) {
8317
+ // src/chat/sandbox/sandbox.ts
8318
+ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
8319
+ function parseHeaderTransforms(raw) {
8320
+ if (!Array.isArray(raw)) {
8093
8321
  return void 0;
8094
8322
  }
8095
- if (trimmed.length <= 80) {
8096
- return trimmed;
8097
- }
8098
- return `...${trimmed.slice(-77)}`;
8323
+ return raw.filter(
8324
+ (value) => Boolean(value && typeof value === "object")
8325
+ ).map((transform) => ({
8326
+ domain: String(transform.domain ?? "").trim(),
8327
+ headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
8328
+ Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8329
+ ) : {}
8330
+ })).filter(
8331
+ (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
8332
+ );
8099
8333
  }
8100
- function compactStatusText(value, maxLength = 80) {
8101
- if (typeof value !== "string") {
8334
+ function parseEnv(raw) {
8335
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8102
8336
  return void 0;
8103
8337
  }
8104
- const trimmed = value.trim();
8105
- if (!trimmed) {
8106
- return void 0;
8107
- }
8108
- return truncateWithEllipsis(trimmed, maxLength);
8338
+ return Object.fromEntries(
8339
+ Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
8340
+ );
8109
8341
  }
8110
- function readShellToken(command, startIndex) {
8111
- let index = startIndex;
8112
- while (index < command.length && /\s/.test(command[index] ?? "")) {
8113
- index += 1;
8114
- }
8115
- if (index >= command.length) {
8116
- return void 0;
8117
- }
8118
- let token = "";
8119
- let quote;
8120
- while (index < command.length) {
8121
- const char = command[index];
8122
- if (!char) {
8123
- break;
8342
+ function createSandboxWorkspace(sandbox) {
8343
+ return {
8344
+ sandboxId: sandbox.sandboxId,
8345
+ readFileToBuffer(input) {
8346
+ return sandbox.readFileToBuffer(input);
8347
+ },
8348
+ runCommand(input) {
8349
+ return sandbox.runCommand(input);
8124
8350
  }
8125
- if (quote) {
8126
- if (char === quote) {
8127
- quote = void 0;
8128
- index += 1;
8129
- continue;
8351
+ };
8352
+ }
8353
+ function createSandboxExecutor(options) {
8354
+ let availableSkills = [];
8355
+ const traceContext = options?.traceContext ?? {};
8356
+ const sessionManager = createSandboxSessionManager({
8357
+ sandboxId: options?.sandboxId,
8358
+ sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8359
+ timeoutMs: options?.timeoutMs,
8360
+ traceContext,
8361
+ onStatus: options?.onStatus
8362
+ });
8363
+ const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8364
+ const logSandboxBootRequest = (trigger, details = {}) => {
8365
+ if (sessionManager.getSandboxId()) {
8366
+ return;
8367
+ }
8368
+ logInfo(
8369
+ "sandbox_boot_requested",
8370
+ traceContext,
8371
+ {
8372
+ "app.sandbox.boot.trigger": trigger,
8373
+ ...details
8374
+ },
8375
+ "Sandbox boot requested"
8376
+ );
8377
+ };
8378
+ const executeBashTool = async (rawInput, command) => {
8379
+ const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
8380
+ const env = parseEnv(rawInput.env);
8381
+ logSandboxBootRequest("tool.bash", {
8382
+ "app.sandbox.command_length": command.length
8383
+ });
8384
+ const executeBash = (await sessionManager.ensureToolExecutors()).bash;
8385
+ const result = await withSandboxSpan(
8386
+ "bash",
8387
+ "process.exec",
8388
+ {
8389
+ "process.executable.name": "bash"
8390
+ },
8391
+ async () => {
8392
+ try {
8393
+ const response = await executeBash({
8394
+ command,
8395
+ ...headerTransforms ? { headerTransforms } : {},
8396
+ ...env ? { env } : {}
8397
+ });
8398
+ setSpanAttributes({
8399
+ "process.exit.code": response.exitCode,
8400
+ "app.sandbox.stdout_bytes": Buffer.byteLength(
8401
+ response.stdout ?? "",
8402
+ "utf8"
8403
+ ),
8404
+ "app.sandbox.stderr_bytes": Buffer.byteLength(
8405
+ response.stderr ?? "",
8406
+ "utf8"
8407
+ ),
8408
+ ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
8409
+ });
8410
+ setSpanStatus(response.exitCode === 0 ? "ok" : "error");
8411
+ return response;
8412
+ } catch (error) {
8413
+ setSpanAttributes({
8414
+ "error.type": error instanceof Error ? error.name : "sandbox_execute_error"
8415
+ });
8416
+ setSpanStatus("error");
8417
+ throw error;
8418
+ }
8130
8419
  }
8131
- if (char === "\\" && quote === '"' && index + 1 < command.length) {
8132
- token += command[index + 1];
8133
- index += 2;
8134
- continue;
8420
+ );
8421
+ return {
8422
+ result: {
8423
+ ok: result.exitCode === 0,
8424
+ command,
8425
+ cwd: SANDBOX_WORKSPACE_ROOT,
8426
+ exit_code: result.exitCode,
8427
+ signal: null,
8428
+ timed_out: false,
8429
+ stdout: result.stdout,
8430
+ stderr: result.stderr,
8431
+ stdout_truncated: result.stdoutTruncated,
8432
+ stderr_truncated: result.stderrTruncated
8433
+ }
8434
+ };
8435
+ };
8436
+ const executeReadFileTool = async (rawInput) => {
8437
+ const filePath = String(rawInput.path ?? "").trim();
8438
+ if (!filePath) {
8439
+ throw new Error("path is required");
8440
+ }
8441
+ if (!sessionManager.getSandboxId()) {
8442
+ const hostSkillPath = resolveHostSkillPath(availableSkills, filePath);
8443
+ if (hostSkillPath) {
8444
+ try {
8445
+ const content = await fs4.readFile(hostSkillPath, "utf8");
8446
+ setSpanAttributes({
8447
+ "app.sandbox.path.length": filePath.length,
8448
+ "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8449
+ "app.sandbox.read.chars": content.length,
8450
+ "app.skill.virtual_read": true
8451
+ });
8452
+ setSpanStatus("ok");
8453
+ return {
8454
+ result: {
8455
+ content,
8456
+ path: filePath,
8457
+ success: true
8458
+ }
8459
+ };
8460
+ } catch (error) {
8461
+ if (!isHostFileMissingError(error)) {
8462
+ throw error;
8463
+ }
8464
+ }
8465
+ }
8466
+ }
8467
+ logSandboxBootRequest("tool.readFile", {
8468
+ "file.path": filePath
8469
+ });
8470
+ const executeReadFile = (await sessionManager.ensureToolExecutors()).readFile;
8471
+ const result = await withSandboxSpan(
8472
+ "sandbox.readFile",
8473
+ "sandbox.fs.read",
8474
+ {
8475
+ "app.sandbox.path.length": filePath.length
8476
+ },
8477
+ async () => {
8478
+ const response = await executeReadFile({ path: filePath });
8479
+ const content = String(response.content ?? "");
8480
+ setSpanAttributes({
8481
+ "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
8482
+ "app.sandbox.read.chars": content.length
8483
+ });
8484
+ setSpanStatus("ok");
8485
+ return {
8486
+ content,
8487
+ path: filePath,
8488
+ success: true
8489
+ };
8490
+ }
8491
+ );
8492
+ return { result };
8493
+ };
8494
+ const executeWriteFileTool = async (rawInput) => {
8495
+ const filePath = String(rawInput.path ?? "").trim();
8496
+ if (!filePath) {
8497
+ throw new Error("path is required");
8498
+ }
8499
+ const content = String(rawInput.content ?? "");
8500
+ logSandboxBootRequest("tool.writeFile", {
8501
+ "file.path": filePath
8502
+ });
8503
+ const executeWriteFile = (await sessionManager.ensureToolExecutors()).writeFile;
8504
+ await withSandboxSpan(
8505
+ "sandbox.writeFile",
8506
+ "sandbox.fs.write",
8507
+ {
8508
+ "app.sandbox.path.length": filePath.length,
8509
+ "app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
8510
+ },
8511
+ async () => {
8512
+ try {
8513
+ await executeWriteFile({ path: filePath, content });
8514
+ } catch (error) {
8515
+ throwSandboxOperationError("sandbox writeFile", error);
8516
+ }
8517
+ setSpanStatus("ok");
8518
+ }
8519
+ );
8520
+ return {
8521
+ result: {
8522
+ ok: true,
8523
+ path: filePath,
8524
+ bytes_written: Buffer.byteLength(content, "utf8")
8525
+ }
8526
+ };
8527
+ };
8528
+ const execute = async (params) => {
8529
+ const rawInput = params.input ?? {};
8530
+ const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
8531
+ if (params.toolName === "bash") {
8532
+ if (!bashCommand) {
8533
+ throw new Error("command is required");
8534
+ }
8535
+ if (options?.runBashCustomCommand) {
8536
+ const custom = await options.runBashCustomCommand(bashCommand);
8537
+ if (custom.handled) {
8538
+ return { result: custom.result };
8539
+ }
8135
8540
  }
8136
- token += char;
8137
- index += 1;
8138
- continue;
8139
- }
8140
- if (/\s/.test(char)) {
8141
- break;
8142
- }
8143
- if (char === '"' || char === "'") {
8144
- quote = char;
8145
- index += 1;
8146
- continue;
8147
- }
8148
- if (char === "\\" && index + 1 < command.length) {
8149
- token += command[index + 1];
8150
- index += 2;
8151
- continue;
8152
- }
8153
- token += char;
8154
- index += 1;
8155
- }
8156
- return { token, nextIndex: index };
8157
- }
8158
- function compactStatusCommand(value) {
8159
- if (typeof value !== "string") {
8160
- return void 0;
8161
- }
8162
- const trimmed = value.trim();
8163
- if (!trimmed) {
8164
- return void 0;
8165
- }
8166
- let index = 0;
8167
- while (index < trimmed.length) {
8168
- const parsed = readShellToken(trimmed, index);
8169
- if (!parsed) {
8170
- return void 0;
8541
+ return await executeBashTool(rawInput, bashCommand);
8171
8542
  }
8172
- index = parsed.nextIndex;
8173
- if (!parsed.token) {
8174
- continue;
8543
+ if (params.toolName === "readFile") {
8544
+ return await executeReadFileTool(rawInput);
8175
8545
  }
8176
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
8177
- continue;
8546
+ if (params.toolName === "writeFile") {
8547
+ return await executeWriteFileTool(rawInput);
8178
8548
  }
8179
- const normalized = parsed.token.replace(/[\\/]+$/g, "");
8180
- if (!normalized) {
8181
- return void 0;
8549
+ throw new Error(`unsupported sandbox tool: ${params.toolName}`);
8550
+ };
8551
+ return {
8552
+ configureSkills(skills) {
8553
+ availableSkills = [...skills];
8554
+ sessionManager.configureSkills(skills);
8555
+ },
8556
+ getSandboxId() {
8557
+ return sessionManager.getSandboxId();
8558
+ },
8559
+ getDependencyProfileHash() {
8560
+ return sessionManager.getDependencyProfileHash();
8561
+ },
8562
+ canExecute(toolName) {
8563
+ return SANDBOX_TOOL_NAMES.has(toolName);
8564
+ },
8565
+ async createSandbox() {
8566
+ return createSandboxWorkspace(await sessionManager.createSandbox());
8567
+ },
8568
+ execute,
8569
+ async dispose() {
8570
+ await sessionManager.dispose();
8182
8571
  }
8183
- const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
8184
- const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
8185
- return compactStatusText(command, 40);
8186
- }
8187
- return void 0;
8188
- }
8189
- function compactStatusFilename(value) {
8190
- if (typeof value !== "string") {
8191
- return void 0;
8192
- }
8193
- const trimmed = value.trim().replace(/[\\/]+$/g, "");
8194
- if (!trimmed) {
8195
- return void 0;
8196
- }
8197
- const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
8198
- const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
8199
- return compactStatusText(filename, 80);
8572
+ };
8200
8573
  }
8201
- function extractStatusUrlDomain(value) {
8202
- if (typeof value !== "string") {
8203
- return void 0;
8204
- }
8205
- const trimmed = value.trim();
8206
- if (!trimmed) {
8207
- return void 0;
8208
- }
8209
- try {
8210
- const parsed = new URL(trimmed);
8211
- return parsed.hostname || void 0;
8212
- } catch {
8213
- return void 0;
8214
- }
8574
+
8575
+ // src/chat/runtime/dev-agent-trace.ts
8576
+ function shouldEmitDevAgentTrace() {
8577
+ return process.env.NODE_ENV === "development";
8215
8578
  }
8216
8579
 
8217
8580
  // src/chat/runtime/tool-status.ts
8218
- function formatToolStatus(toolName) {
8219
- const known = {
8220
- loadSkill: "Loading skill instructions",
8221
- systemTime: "Reading current system time",
8222
- bash: "Working in the shell",
8223
- readFile: "Reading a file",
8224
- writeFile: "Updating a file",
8225
- webSearch: "Searching public sources",
8226
- webFetch: "Reading source pages",
8227
- slackChannelPostMessage: "Posting message to channel",
8228
- slackMessageAddReaction: "Adding emoji reaction",
8229
- slackChannelListMessages: "Listing channel messages",
8230
- slackCanvasCreate: "Creating detailed brief",
8231
- slackCanvasUpdate: "Updating detailed brief",
8232
- slackListCreate: "Creating tracking list",
8233
- slackListAddItems: "Updating tracking list",
8234
- slackListUpdateItem: "Updating tracking list",
8235
- imageGenerate: "Generating image",
8236
- searchTools: "Searching active tools"
8237
- };
8238
- if (known[toolName]) {
8239
- return known[toolName];
8240
- }
8241
- const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(toolName);
8242
- if (mcpMatch) {
8243
- return `Running ${mcpMatch[1]}/${mcpMatch[2]}`;
8244
- }
8245
- const readable = toolName.replaceAll("_", " ").trim();
8246
- return readable.length > 0 ? `Running ${readable}` : "Running tool";
8247
- }
8248
- function formatToolStatusWithInput(toolName, input) {
8581
+ function buildToolStatus(toolName, input) {
8249
8582
  const obj = input && typeof input === "object" ? input : void 0;
8250
8583
  const command = obj ? compactStatusCommand(obj.command) : void 0;
8251
8584
  const path6 = obj ? compactStatusPath(obj.path) : void 0;
@@ -8255,33 +8588,63 @@ function formatToolStatusWithInput(toolName, input) {
8255
8588
  const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
8256
8589
  const provider = obj ? compactStatusText(obj.provider, 20) : void 0;
8257
8590
  if (command && toolName === "bash") {
8258
- return `Running ${command}`;
8591
+ return makeAssistantStatus("running", command);
8259
8592
  }
8260
8593
  if (filename && toolName === "readFile") {
8261
- return `Reading file ${filename}`;
8594
+ return makeAssistantStatus("reading", filename);
8262
8595
  }
8263
8596
  if (filename && toolName === "writeFile") {
8264
- return `Updating file ${filename}`;
8597
+ return makeAssistantStatus("updating", filename);
8265
8598
  }
8266
8599
  if (path6 && toolName === "writeFile") {
8267
- return `Updating file ${path6}`;
8600
+ return makeAssistantStatus("updating", path6);
8268
8601
  }
8269
8602
  if (skillName && toolName === "loadSkill") {
8270
- return `Loading skill ${skillName}`;
8603
+ return makeAssistantStatus("loading", skillName);
8271
8604
  }
8272
8605
  if (query && toolName === "webSearch") {
8273
- return `Searching web for "${query}"`;
8606
+ return makeAssistantStatus("searching", `"${query}"`);
8274
8607
  }
8275
8608
  if (query && provider && toolName === "searchTools") {
8276
- return `Searching ${provider} tools for "${query}"`;
8609
+ return makeAssistantStatus("searching", `${provider} "${query}"`);
8277
8610
  }
8278
8611
  if (query && toolName === "searchTools") {
8279
- return `Searching tools for "${query}"`;
8612
+ return makeAssistantStatus("searching", `"${query}"`);
8280
8613
  }
8281
8614
  if (domain && toolName === "webFetch") {
8282
- return `Fetching page from ${domain}`;
8615
+ return makeAssistantStatus("fetching", domain);
8616
+ }
8617
+ const known = {
8618
+ loadSkill: makeAssistantStatus("loading", "skill instructions"),
8619
+ systemTime: makeAssistantStatus("reading", "system time"),
8620
+ bash: makeAssistantStatus("running", "shell"),
8621
+ readFile: makeAssistantStatus("reading", "file"),
8622
+ writeFile: makeAssistantStatus("updating", "file"),
8623
+ webSearch: makeAssistantStatus("searching", "sources"),
8624
+ webFetch: makeAssistantStatus("fetching", "pages"),
8625
+ slackChannelPostMessage: makeAssistantStatus("posting", "channel"),
8626
+ slackMessageAddReaction: makeAssistantStatus("adding", "reaction"),
8627
+ slackChannelListMessages: makeAssistantStatus("listing", "messages"),
8628
+ slackCanvasCreate: makeAssistantStatus("creating", "brief"),
8629
+ slackCanvasUpdate: makeAssistantStatus("updating", "brief"),
8630
+ slackListCreate: makeAssistantStatus("creating", "tracking list"),
8631
+ slackListAddItems: makeAssistantStatus("updating", "tracking list"),
8632
+ slackListUpdateItem: makeAssistantStatus("updating", "tracking list"),
8633
+ imageGenerate: makeAssistantStatus("creating", "image"),
8634
+ searchTools: makeAssistantStatus(
8635
+ "searching",
8636
+ provider ? `${provider} tools` : "tools"
8637
+ )
8638
+ };
8639
+ if (known[toolName]) {
8640
+ return known[toolName];
8641
+ }
8642
+ const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(toolName);
8643
+ if (mcpMatch) {
8644
+ return makeAssistantStatus("running", `${mcpMatch[1]}/${mcpMatch[2]}`);
8283
8645
  }
8284
- return formatToolStatus(toolName);
8646
+ const readable = toolName.replaceAll("_", " ").trim();
8647
+ return makeAssistantStatus("running", readable || "tool");
8285
8648
  }
8286
8649
 
8287
8650
  // src/chat/tools/execution/build-sandbox-input.ts
@@ -8432,7 +8795,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8432
8795
  turnId: spanContext.turnId,
8433
8796
  agentId: spanContext.agentId
8434
8797
  };
8435
- await onStatus?.(`${formatToolStatusWithInput(toolName, params)}...`);
8798
+ await onStatus?.(buildToolStatus(toolName, params));
8436
8799
  return withSpan(
8437
8800
  `execute_tool ${toolName}`,
8438
8801
  "gen_ai.execute_tool",
@@ -9027,6 +9390,14 @@ async function generateAssistantReply(messageText, context = {}) {
9027
9390
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
9028
9391
  let loadedSkillNamesForResume = [];
9029
9392
  let mcpToolManager;
9393
+ let sandboxExecutor;
9394
+ const getSandboxMetadata = () => sandboxExecutor ? {
9395
+ sandboxId: sandboxExecutor.getSandboxId(),
9396
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
9397
+ } : {
9398
+ sandboxId: lastKnownSandboxId,
9399
+ sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash
9400
+ };
9030
9401
  try {
9031
9402
  const shouldTrace = shouldEmitDevAgentTrace();
9032
9403
  const spanContext = {
@@ -9098,7 +9469,8 @@ async function generateAssistantReply(messageText, context = {}) {
9098
9469
  requesterId: context.requester?.userId,
9099
9470
  resolveConfiguration: async (key) => configurationValues[key]
9100
9471
  });
9101
- const sandboxExecutor = createSandboxExecutor({
9472
+ const providerAuthActions = /* @__PURE__ */ new Map();
9473
+ sandboxExecutor = createSandboxExecutor({
9102
9474
  sandboxId: context.sandbox?.sandboxId,
9103
9475
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
9104
9476
  traceContext: spanContext,
@@ -9113,6 +9485,7 @@ async function generateAssistantReply(messageText, context = {}) {
9113
9485
  threadTs: context.correlation?.threadTs,
9114
9486
  userMessage: userInput,
9115
9487
  userTokenStore: createUserTokenStore(),
9488
+ providerAuthActions,
9116
9489
  onConfigurationValueChanged: (key, value) => {
9117
9490
  if (value === void 0) {
9118
9491
  delete configurationValues[key];
@@ -9124,10 +9497,52 @@ async function generateAssistantReply(messageText, context = {}) {
9124
9497
  return result.handled ? { handled: true, result: result.result } : { handled: false };
9125
9498
  }
9126
9499
  });
9127
- lastKnownSandboxId = sandboxExecutor.getSandboxId();
9128
- lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
9500
+ const currentSandboxExecutor = sandboxExecutor;
9129
9501
  sandboxExecutor.configureSkills(availableSkills);
9130
- const sandbox = await sandboxExecutor.createSandbox();
9502
+ let sandboxPromise;
9503
+ let sandboxPromiseId;
9504
+ const clearSandboxPromise = () => {
9505
+ sandboxPromise = void 0;
9506
+ sandboxPromiseId = void 0;
9507
+ };
9508
+ const getSandbox = (reason) => {
9509
+ const currentSandboxId = currentSandboxExecutor.getSandboxId();
9510
+ if (sandboxPromise && sandboxPromiseId && currentSandboxId !== sandboxPromiseId) {
9511
+ clearSandboxPromise();
9512
+ }
9513
+ if (!sandboxPromise) {
9514
+ logInfo(
9515
+ "sandbox_boot_requested",
9516
+ spanContext,
9517
+ {
9518
+ "app.sandbox.boot.trigger": reason.trigger,
9519
+ ...reason.path ? { "file.path": reason.path } : {},
9520
+ ...reason.cmd ? { "process.executable.name": reason.cmd } : {},
9521
+ ...reason.cwd ? { "file.directory": reason.cwd } : {}
9522
+ },
9523
+ "Lazy sandbox boot requested"
9524
+ );
9525
+ sandboxPromise = currentSandboxExecutor.createSandbox().then((sandbox2) => {
9526
+ sandboxPromiseId = sandbox2.sandboxId;
9527
+ return sandbox2;
9528
+ }).catch((error) => {
9529
+ clearSandboxPromise();
9530
+ throw error;
9531
+ });
9532
+ }
9533
+ return sandboxPromise;
9534
+ };
9535
+ const sandbox = {
9536
+ readFileToBuffer: async (input) => (await getSandbox({
9537
+ trigger: "workspace.readFileToBuffer",
9538
+ path: input.path
9539
+ })).readFileToBuffer(input),
9540
+ runCommand: async (input) => (await getSandbox({
9541
+ trigger: "workspace.runCommand",
9542
+ cmd: input.cmd,
9543
+ cwd: input.cwd
9544
+ })).runCommand(input)
9545
+ };
9131
9546
  for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
9132
9547
  const preloaded = await skillSandbox.loadSkill(skillName);
9133
9548
  if (preloaded) {
@@ -9201,11 +9616,6 @@ async function generateAssistantReply(messageText, context = {}) {
9201
9616
  onArtifactStatePatch: (patch) => {
9202
9617
  Object.assign(artifactStatePatch, patch);
9203
9618
  },
9204
- onToolCallStart: async (toolName, input) => {
9205
- await context.onStatus?.(
9206
- `${formatToolStatusWithInput(toolName, input)}...`
9207
- );
9208
- },
9209
9619
  toolOverrides: context.toolOverrides,
9210
9620
  onSkillLoaded: async (loadedSkill) => {
9211
9621
  const resolvedSkill = await skillSandbox.loadSkill(loadedSkill.name);
@@ -9271,16 +9681,32 @@ async function generateAssistantReply(messageText, context = {}) {
9271
9681
  });
9272
9682
  const userContentParts = [{ type: "text", text: userTurnText }];
9273
9683
  for (const attachment of context.userAttachments ?? []) {
9274
- if (attachment.mediaType.startsWith("image/")) {
9684
+ if (attachment.promptText) {
9685
+ userContentParts.push({
9686
+ type: "text",
9687
+ text: attachment.promptText
9688
+ });
9689
+ } else if (attachment.mediaType.startsWith("image/")) {
9690
+ if (!attachment.data) {
9691
+ throw new Error("Image attachment is missing image data");
9692
+ }
9275
9693
  userContentParts.push({
9276
9694
  type: "image",
9277
9695
  data: attachment.data.toString("base64"),
9278
9696
  mimeType: attachment.mediaType
9279
9697
  });
9280
9698
  } else {
9699
+ if (!attachment.data) {
9700
+ throw new Error("Attachment is missing attachment data");
9701
+ }
9702
+ const promptAttachment = {
9703
+ data: attachment.data,
9704
+ mediaType: attachment.mediaType,
9705
+ filename: attachment.filename
9706
+ };
9281
9707
  userContentParts.push({
9282
9708
  type: "text",
9283
- text: encodeNonImageAttachmentForPrompt(attachment)
9709
+ text: encodeNonImageAttachmentForPrompt(promptAttachment)
9284
9710
  });
9285
9711
  }
9286
9712
  }
@@ -9477,8 +9903,8 @@ async function generateAssistantReply(messageText, context = {}) {
9477
9903
  replyFiles,
9478
9904
  artifactStatePatch,
9479
9905
  toolCalls,
9480
- sandboxId: sandboxExecutor.getSandboxId(),
9481
- sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
9906
+ sandboxId: currentSandboxExecutor.getSandboxId(),
9907
+ sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
9482
9908
  generatedFileCount: generatedFiles.length,
9483
9909
  hasTextDeltaCallback: Boolean(context.onTextDelta),
9484
9910
  shouldTrace,
@@ -9529,8 +9955,7 @@ async function generateAssistantReply(messageText, context = {}) {
9529
9955
  const message = error instanceof Error ? error.message : String(error);
9530
9956
  return {
9531
9957
  text: `Error: ${message}`,
9532
- sandboxId: lastKnownSandboxId,
9533
- sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
9958
+ ...getSandboxMetadata(),
9534
9959
  diagnostics: {
9535
9960
  outcome: "provider_error",
9536
9961
  modelId: botConfig.modelId,
@@ -9559,90 +9984,173 @@ async function generateAssistantReply(messageText, context = {}) {
9559
9984
  }
9560
9985
  }
9561
9986
 
9562
- // src/handlers/oauth-resume.ts
9563
- function resolveReplyTimeoutMs(explicitTimeoutMs) {
9564
- if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
9565
- return explicitTimeoutMs;
9566
- }
9567
- const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
9568
- if (!raw) {
9569
- return void 0;
9570
- }
9571
- const parsed = Number.parseInt(raw, 10);
9572
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
9573
- }
9574
- async function postSlackMessage(channelId, threadTs, text) {
9575
- try {
9576
- await getSlackClient().chat.postMessage({
9577
- channel: channelId,
9578
- thread_ts: threadTs,
9579
- text
9580
- });
9581
- } catch {
9582
- }
9583
- }
9584
- async function setAssistantStatus(channelId, threadTs, status) {
9585
- try {
9586
- await getSlackClient().assistant.threads.setStatus({
9587
- channel_id: channelId,
9588
- thread_ts: threadTs,
9589
- status
9590
- });
9591
- } catch {
9592
- }
9593
- }
9594
- var STATUS_DEBOUNCE_MS = 1e3;
9595
- function createDebouncedStatusPoster(channelId, threadTs) {
9596
- let lastPostAt = 0;
9597
- let currentStatus = "";
9987
+ // src/chat/runtime/progress-reporter.ts
9988
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
9989
+ var STATUS_MIN_VISIBLE_MS = 1200;
9990
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
9991
+ function createProgressReporter(args) {
9992
+ const now = args.now ?? (() => Date.now());
9993
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
9994
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
9995
+ const random = args.random ?? Math.random;
9996
+ let active = false;
9997
+ let currentKey = "";
9998
+ let currentStatus = makeAssistantStatus("thinking");
9999
+ let currentVisibleStatus = "";
10000
+ let lastStatusAt = 0;
9598
10001
  let pendingStatus = null;
10002
+ let pendingKey = "";
9599
10003
  let pendingTimer = null;
9600
- let stopped = false;
9601
- const flush = async () => {
9602
- if (stopped || !pendingStatus) return;
9603
- const status = pendingStatus;
9604
- pendingStatus = null;
9605
- pendingTimer = null;
9606
- lastPostAt = Date.now();
9607
- currentStatus = status;
9608
- await setAssistantStatus(channelId, threadTs, status);
9609
- };
9610
- const post = async (status) => {
9611
- if (stopped) return;
9612
- const truncated = truncateStatusText(status);
9613
- if (!truncated || truncated === currentStatus) return;
9614
- const now = Date.now();
9615
- const elapsed = now - lastPostAt;
9616
- if (elapsed >= STATUS_DEBOUNCE_MS) {
9617
- if (pendingTimer) {
9618
- clearTimeout(pendingTimer);
9619
- pendingTimer = null;
10004
+ let rotationTimer = null;
10005
+ let inflightStatusUpdate = Promise.resolve();
10006
+ const scheduleRotation = () => {
10007
+ if (rotationTimer) {
10008
+ clearTimer(rotationTimer);
10009
+ rotationTimer = null;
10010
+ }
10011
+ if (!active || !currentVisibleStatus) {
10012
+ return;
10013
+ }
10014
+ rotationTimer = setTimer(() => {
10015
+ rotationTimer = null;
10016
+ if (!active || !currentVisibleStatus) {
10017
+ return;
9620
10018
  }
9621
- pendingStatus = null;
9622
- lastPostAt = now;
9623
- currentStatus = truncated;
9624
- await setAssistantStatus(channelId, threadTs, truncated);
10019
+ void postRenderedStatus(currentStatus);
10020
+ }, STATUS_ROTATION_INTERVAL_MS);
10021
+ };
10022
+ const postStatus = async (text, suggestions) => {
10023
+ const channelId = args.channelId;
10024
+ const threadTs = args.threadTs;
10025
+ if (!channelId || !threadTs) {
9625
10026
  return;
9626
10027
  }
9627
- pendingStatus = truncated;
9628
- if (!pendingTimer) {
9629
- pendingTimer = setTimeout(
9630
- () => {
9631
- void flush();
9632
- },
9633
- Math.max(1, STATUS_DEBOUNCE_MS - elapsed)
9634
- );
10028
+ if (!text && !currentVisibleStatus) {
10029
+ return;
9635
10030
  }
10031
+ currentVisibleStatus = text;
10032
+ lastStatusAt = now();
10033
+ scheduleRotation();
10034
+ const previous = inflightStatusUpdate;
10035
+ const request = (async () => {
10036
+ await previous;
10037
+ await args.transport.setStatus(channelId, threadTs, text, suggestions);
10038
+ })();
10039
+ inflightStatusUpdate = request;
10040
+ await request;
10041
+ };
10042
+ const postRenderedStatus = async (status) => {
10043
+ const presentation = buildAssistantStatusPresentation({
10044
+ status,
10045
+ random
10046
+ });
10047
+ currentStatus = status;
10048
+ currentKey = presentation.key;
10049
+ await postStatus(presentation.visible, presentation.suggestions);
9636
10050
  };
9637
- post.stop = () => {
9638
- stopped = true;
10051
+ const clearPending = () => {
9639
10052
  if (pendingTimer) {
9640
- clearTimeout(pendingTimer);
10053
+ clearTimer(pendingTimer);
9641
10054
  pendingTimer = null;
9642
10055
  }
9643
10056
  pendingStatus = null;
10057
+ pendingKey = "";
10058
+ };
10059
+ const flushPending = async () => {
10060
+ if (!active || !pendingStatus) {
10061
+ clearPending();
10062
+ return;
10063
+ }
10064
+ const next = pendingStatus;
10065
+ clearPending();
10066
+ const nextPresentation = buildAssistantStatusPresentation({
10067
+ status: next,
10068
+ random
10069
+ });
10070
+ if (nextPresentation.key !== currentKey) {
10071
+ await postRenderedStatus(next);
10072
+ }
10073
+ };
10074
+ return {
10075
+ async start() {
10076
+ active = true;
10077
+ clearPending();
10078
+ currentStatus = makeAssistantStatus("thinking");
10079
+ currentKey = "";
10080
+ void postRenderedStatus(currentStatus);
10081
+ },
10082
+ async stop() {
10083
+ active = false;
10084
+ clearPending();
10085
+ if (rotationTimer) {
10086
+ clearTimer(rotationTimer);
10087
+ rotationTimer = null;
10088
+ }
10089
+ currentKey = "";
10090
+ await postStatus("");
10091
+ },
10092
+ async setStatus(status) {
10093
+ if (!active) {
10094
+ return;
10095
+ }
10096
+ const presentation = buildAssistantStatusPresentation({
10097
+ status,
10098
+ random
10099
+ });
10100
+ if (!presentation.visible) {
10101
+ return;
10102
+ }
10103
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
10104
+ return;
10105
+ }
10106
+ const elapsed = now() - lastStatusAt;
10107
+ const waitMs = Math.max(
10108
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
10109
+ STATUS_MIN_VISIBLE_MS - elapsed,
10110
+ 0
10111
+ );
10112
+ if (waitMs <= 0) {
10113
+ clearPending();
10114
+ void postRenderedStatus(status);
10115
+ return;
10116
+ }
10117
+ pendingStatus = status;
10118
+ pendingKey = presentation.key;
10119
+ if (pendingTimer) {
10120
+ return;
10121
+ }
10122
+ pendingTimer = setTimer(
10123
+ () => {
10124
+ pendingTimer = null;
10125
+ void flushPending();
10126
+ },
10127
+ Math.max(1, waitMs)
10128
+ );
10129
+ }
9644
10130
  };
9645
- return post;
10131
+ }
10132
+
10133
+ // src/handlers/oauth-resume.ts
10134
+ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10135
+ if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
10136
+ return explicitTimeoutMs;
10137
+ }
10138
+ const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
10139
+ if (!raw) {
10140
+ return void 0;
10141
+ }
10142
+ const parsed = Number.parseInt(raw, 10);
10143
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10144
+ }
10145
+ async function postSlackMessage(channelId, threadTs, text) {
10146
+ try {
10147
+ await getSlackClient().chat.postMessage({
10148
+ channel: channelId,
10149
+ thread_ts: threadTs,
10150
+ text
10151
+ });
10152
+ } catch {
10153
+ }
9646
10154
  }
9647
10155
  function createReadOnlyConfigService(values) {
9648
10156
  const entries = Object.entries(values).map(([key, value]) => ({
@@ -9671,9 +10179,13 @@ function createReadOnlyConfigService(values) {
9671
10179
  };
9672
10180
  }
9673
10181
  async function resumeAuthorizedRequest(args) {
9674
- const postStatus = createDebouncedStatusPoster(args.channelId, args.threadTs);
10182
+ const progress = createProgressReporter({
10183
+ channelId: args.channelId,
10184
+ threadTs: args.threadTs,
10185
+ transport: createSlackWebApiAssistantStatusTransport()
10186
+ });
9675
10187
  await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
9676
- await setAssistantStatus(args.channelId, args.threadTs, "Thinking...");
10188
+ await progress.start();
9677
10189
  try {
9678
10190
  const generateReply = args.generateReply ?? generateAssistantReply;
9679
10191
  const replyPromise = generateReply(args.messageText, {
@@ -9691,7 +10203,7 @@ async function resumeAuthorizedRequest(args) {
9691
10203
  artifactState: args.artifactState,
9692
10204
  configuration: args.configuration,
9693
10205
  channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
9694
- onStatus: postStatus
10206
+ onStatus: (status) => progress.setStatus(status)
9695
10207
  });
9696
10208
  const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
9697
10209
  const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
@@ -9707,8 +10219,7 @@ async function resumeAuthorizedRequest(args) {
9707
10219
  )
9708
10220
  )
9709
10221
  ]) : await replyPromise;
9710
- postStatus.stop();
9711
- await setAssistantStatus(args.channelId, args.threadTs, "");
10222
+ await progress.stop();
9712
10223
  if (args.onReply) {
9713
10224
  await args.onReply(reply);
9714
10225
  } else if (reply.text) {
@@ -9716,8 +10227,7 @@ async function resumeAuthorizedRequest(args) {
9716
10227
  }
9717
10228
  await args.onSuccess?.(reply);
9718
10229
  } catch (error) {
9719
- postStatus.stop();
9720
- await setAssistantStatus(args.channelId, args.threadTs, "");
10230
+ await progress.stop();
9721
10231
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
9722
10232
  await args.onAuthPause(error);
9723
10233
  return;
@@ -10049,7 +10559,7 @@ async function GET4(request, provider, waitUntil) {
10049
10559
  }
10050
10560
 
10051
10561
  // src/chat/slack/app-home.ts
10052
- import fs4 from "fs";
10562
+ import fs5 from "fs";
10053
10563
  import path5 from "path";
10054
10564
  var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
10055
10565
  var MAX_HOME_SKILLS = 6;
@@ -10064,7 +10574,7 @@ function clampSectionText(text) {
10064
10574
  function loadAboutText() {
10065
10575
  const aboutPath = path5.join(homeDir(), "ABOUT.md");
10066
10576
  try {
10067
- const raw = fs4.readFileSync(aboutPath, "utf8").trim();
10577
+ const raw = fs5.readFileSync(aboutPath, "utf8").trim();
10068
10578
  if (raw.length > 0) {
10069
10579
  return clampSectionText(raw);
10070
10580
  }
@@ -10414,7 +10924,7 @@ var replyDecisionSchema = z.object({
10414
10924
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
10415
10925
  var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
10416
10926
  var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
10417
- var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]+:\s+([\s\S]+)$/i;
10927
+ var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
10418
10928
  var THREAD_OPTOUT_PATTERNS = [
10419
10929
  /\bstop (?:watching|replying|participating)\b/i,
10420
10930
  /\bstay out\b/i,
@@ -10422,6 +10932,11 @@ var THREAD_OPTOUT_PATTERNS = [
10422
10932
  /\bunsubscribe\b/i,
10423
10933
  /\bleave (?:this )?thread\b/i
10424
10934
  ];
10935
+ var ACKNOWLEDGMENT_ONLY_RE = /^(?:thanks(?: you)?|thank you|thx|ty|got it|sounds good|sgtm|lgtm|ok(?:ay)?|cool|nice|perfect|awesome|great|makes sense|understood|roger|yep|yup|kk|on it|will do)(?:[.!]+)?$/i;
10936
+ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|your last answer|what did you just say|what do you mean|what did you mean|explain(?: that| this| it| more)?|clarify(?: that| this| it)?|expand(?: on)?(?: that| this| it)?|elaborate(?: on)?(?: that| this| it)?|say more)\b/i;
10937
+ var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
10938
+ var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
10939
+ var RECENT_THREAD_WINDOW = 6;
10425
10940
  function escapeRegExp(value) {
10426
10941
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10427
10942
  }
@@ -10458,36 +10973,113 @@ function isThreadOptOutInstruction(rawText, text) {
10458
10973
  (pattern) => pattern.test(rawText) || pattern.test(text)
10459
10974
  );
10460
10975
  }
10461
- function getTranscriptMessageHints(conversationContext) {
10976
+ function isAcknowledgmentOnly(text) {
10977
+ return ACKNOWLEDGMENT_ONLY_RE.test(text.trim());
10978
+ }
10979
+ function hasDirectedFollowUpCue(text) {
10980
+ return DIRECTED_FOLLOW_UP_CUE_RE.test(text.trim());
10981
+ }
10982
+ function isTerseClarification(text) {
10983
+ return TERSE_CLARIFICATION_RE.test(text.trim());
10984
+ }
10985
+ function isGenericImmediateSideConversation(text) {
10986
+ const trimmed = text.trim();
10987
+ if (GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE.test(trimmed)) {
10988
+ return true;
10989
+ }
10990
+ if (!trimmed.toLowerCase().startsWith("what about")) {
10991
+ return false;
10992
+ }
10993
+ const wordCount = trimmed.split(/\s+/).map((part) => part.trim()).filter(Boolean).length;
10994
+ return wordCount > 3;
10995
+ }
10996
+ function parseTranscriptMessages(conversationContext) {
10462
10997
  if (!conversationContext) {
10463
- return {
10464
- latestPriorMessageRole: "[none]",
10465
- latestPriorAssistantMessage: "[none]"
10466
- };
10998
+ return [];
10467
10999
  }
11000
+ const messages = [];
10468
11001
  const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
10469
- let latestPriorMessageRole = "[none]";
10470
- let latestPriorAssistantMessage = "[none]";
10471
- for (let index = lines.length - 1; index >= 0; index -= 1) {
10472
- const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
11002
+ for (const line of lines) {
11003
+ const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
10473
11004
  if (!match) {
10474
11005
  continue;
10475
11006
  }
10476
- if (latestPriorMessageRole === "[none]") {
10477
- latestPriorMessageRole = match[1].toLowerCase();
10478
- }
10479
- if (latestPriorAssistantMessage === "[none]" && match[1].toLowerCase() === "assistant") {
10480
- latestPriorAssistantMessage = match[2];
11007
+ messages.push({
11008
+ role: match[1].toLowerCase(),
11009
+ author: match[2]?.trim() || "unknown",
11010
+ text: match[3]?.trim() || ""
11011
+ });
11012
+ }
11013
+ return messages;
11014
+ }
11015
+ function buildRouterSignals(input) {
11016
+ const transcriptMessages = parseTranscriptMessages(input.conversationContext);
11017
+ const recentMessages = transcriptMessages.filter((message) => message.role !== "system").slice(-RECENT_THREAD_WINDOW);
11018
+ const latestPriorMessage = [...transcriptMessages].reverse().find((message) => message.role !== "system");
11019
+ const latestPriorAssistantMessage = [...transcriptMessages].reverse().find((message) => message.role === "assistant");
11020
+ let humanMessagesSinceLastAssistant;
11021
+ let humanMessageCount = 0;
11022
+ for (let index = transcriptMessages.length - 1; index >= 0; index -= 1) {
11023
+ const message = transcriptMessages[index];
11024
+ if (!message || message.role === "system") {
11025
+ continue;
10481
11026
  }
10482
- if (latestPriorMessageRole !== "[none]" && latestPriorAssistantMessage !== "[none]") {
11027
+ if (message.role === "assistant") {
11028
+ humanMessagesSinceLastAssistant = humanMessageCount;
10483
11029
  break;
10484
11030
  }
11031
+ humanMessageCount += 1;
10485
11032
  }
10486
11033
  return {
10487
- latestPriorMessageRole,
10488
- latestPriorAssistantMessage
11034
+ assistantWasLastSpeaker: latestPriorMessage?.role === "assistant",
11035
+ currentMessageHasDirectedFollowUpCue: hasDirectedFollowUpCue(input.text),
11036
+ currentMessageHasAttachments: Boolean(input.hasAttachments),
11037
+ currentMessageIsTerseClarification: isTerseClarification(input.text),
11038
+ humanMessagesSinceLastAssistant,
11039
+ latestPriorAssistantMessage: latestPriorAssistantMessage?.text || "[none]",
11040
+ latestPriorMessageRole: latestPriorMessage?.role || "[none]",
11041
+ recentMessages
10489
11042
  };
10490
11043
  }
11044
+ function buildRouterPrompt(rawText, signals) {
11045
+ const recentThread = signals.recentMessages.length > 0 ? signals.recentMessages.map(
11046
+ (message) => escapeXml(`[${message.role}] ${message.author}: ${message.text}`)
11047
+ ).join("\n") : "[none]";
11048
+ return [
11049
+ `<latest-message>${escapeXml(rawText.trim() || "[attachment-only message]")}</latest-message>`,
11050
+ "<routing-signals>",
11051
+ `assistant_was_last_speaker=${signals.assistantWasLastSpeaker ? "true" : "false"}`,
11052
+ `human_messages_since_last_assistant=${signals.humanMessagesSinceLastAssistant ?? "none"}`,
11053
+ `latest_prior_message_role=${escapeXml(signals.latestPriorMessageRole)}`,
11054
+ `current_message_has_directed_follow_up_cue=${signals.currentMessageHasDirectedFollowUpCue ? "true" : "false"}`,
11055
+ `current_message_is_terse_clarification=${signals.currentMessageIsTerseClarification ? "true" : "false"}`,
11056
+ `current_message_has_attachments=${signals.currentMessageHasAttachments ? "true" : "false"}`,
11057
+ "</routing-signals>",
11058
+ `<latest-prior-assistant-message>${escapeXml(
11059
+ signals.latestPriorAssistantMessage
11060
+ )}</latest-prior-assistant-message>`,
11061
+ "<recent-thread>",
11062
+ recentThread,
11063
+ "</recent-thread>"
11064
+ ].join("\n");
11065
+ }
11066
+ function getReplyConfidenceThreshold(signals) {
11067
+ let threshold = ROUTER_CONFIDENCE_THRESHOLD;
11068
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0) {
11069
+ if (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification) {
11070
+ threshold = 0.65;
11071
+ } else {
11072
+ threshold = 0.9;
11073
+ }
11074
+ } else if (signals.humanMessagesSinceLastAssistant === 1) {
11075
+ threshold = signals.currentMessageHasDirectedFollowUpCue ? 0.8 : 0.9;
11076
+ } else if (signals.humanMessagesSinceLastAssistant === void 0) {
11077
+ threshold = 0.85;
11078
+ } else if (signals.humanMessagesSinceLastAssistant >= 2) {
11079
+ threshold = 0.9;
11080
+ }
11081
+ return Math.max(0.6, Math.min(0.9, threshold));
11082
+ }
10491
11083
  function getSubscribedReplyPreflightDecision(args) {
10492
11084
  const text = args.text.trim();
10493
11085
  const rawText = args.rawText.trim();
@@ -10508,54 +11100,27 @@ function getSubscribedReplyPreflightDecision(args) {
10508
11100
  reasonDetail: leadingOtherPartyAddress
10509
11101
  };
10510
11102
  }
10511
- function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
10512
- const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
11103
+ function buildRouterSystemPrompt(botUserName) {
10513
11104
  return [
10514
11105
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
10515
11106
  "Decide whether Junior should reply to the latest message.",
10516
11107
  "Subscribed threads are passive by default.",
10517
- "Default to should_reply=false unless the user is clearly asking Junior for help or follow-up.",
10518
- "A direct @mention is a strong signal to reply unless the message is clearly telling Junior to stop participating.",
10519
- "",
10520
- "Reply should be true only when the user is clearly asking Junior a question, requesting help,",
10521
- "or when a direct follow-up is contextually aimed at Junior's previous response in the thread context.",
10522
- "",
10523
- "Reply should be false for side conversations between humans, acknowledgements,",
10524
- "status chatter, or messages not seeking assistant input.",
10525
- "Junior must not participate in casual banter or keep chiming in just because it replied earlier.",
10526
- "",
10527
- "Examples of messages Junior should NOT reply to (should_reply=false):",
10528
- "- Questions between humans: 'Is that the right approach?', 'Can you check on this?', 'Did you deploy that?'",
10529
- "- Acknowledgments: 'thanks', '+1', 'lgtm', 'ok cool', 'sounds good', 'nice'",
10530
- "- Status updates: 'I just pushed a fix', 'Deploying now', 'Build is green'",
10531
- "- General thread discussion: 'What about the billing issue?', 'I think we should revert'",
10532
- "- Reactions to work: 'That looks wrong', 'Nice catch', 'Hmm interesting'",
10533
- "",
10534
- "Examples of messages Junior SHOULD reply to (should_reply=true):",
10535
- "- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
10536
- "- 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?'",
10537
- "- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
10538
- "",
10539
- "Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
10540
- "using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
10541
- "Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
10542
- "still should_reply=false unless it clearly asks Junior for help.",
10543
- "",
10544
- "When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
10545
- "",
10546
- "If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
10547
- "set should_unsubscribe=true and should_reply=false.",
10548
- "Use should_unsubscribe only for clear thread opt-out instructions, not for ordinary side conversation.",
10549
- "If uncertain, set should_reply=false and use low confidence.",
11108
+ "Reply true only when the latest message is aimed at Junior.",
11109
+ "Use who currently has the conversation floor, not just topic overlap.",
11110
+ "If Junior was the last speaker, only a clear turn back to Junior should count as an implicit follow-up.",
11111
+ "Terse clarifications like 'which one?' or 'why?' right after Junior answers can be should_reply=true.",
11112
+ "Direct self-reference to Junior's prior answer like 'what did you just say?' or 'explain that more' can be should_reply=true.",
11113
+ "If one or more humans spoke after Junior, require a clear turn back to Junior. Shared domain vocabulary alone is not enough.",
11114
+ "Questions like 'what about auth?' or 'can you check on this?' are usually human-to-human unless the thread clearly turns back to Junior.",
11115
+ "A vague question like 'is that the right approach?' is still should_reply=false unless it clearly turns back to Junior.",
11116
+ "Acknowledgments, reactions, status chatter, and team coordination should be should_reply=false.",
11117
+ "If the latest message clearly tells Junior to stop watching, replying, or participating, set should_unsubscribe=true and should_reply=false.",
11118
+ "When uncertain, prefer should_reply=false with low confidence.",
10550
11119
  "",
10551
11120
  "Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
10552
11121
  "Do not return any extra keys.",
10553
11122
  "",
10554
- `<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
10555
- `<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
10556
- `<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
10557
- `<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
10558
- `<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
11123
+ `<assistant-name>${escapeXml(botUserName)}</assistant-name>`
10559
11124
  ].join("\n");
10560
11125
  }
10561
11126
  async function decideSubscribedThreadReply(args) {
@@ -10570,11 +11135,16 @@ async function decideSubscribedThreadReply(args) {
10570
11135
  if (preflightDecision) {
10571
11136
  return preflightDecision;
10572
11137
  }
11138
+ const signals = buildRouterSignals(args.input);
10573
11139
  if (!text && !args.input.hasAttachments) {
10574
11140
  return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
10575
11141
  }
10576
- if (!text && args.input.hasAttachments) {
10577
- return { shouldReply: true, reason: "attachment_only" /* AttachmentOnly */ };
11142
+ if (!args.input.isExplicitMention && !args.input.hasAttachments && isAcknowledgmentOnly(text)) {
11143
+ return {
11144
+ shouldReply: false,
11145
+ reason: "side_conversation" /* SideConversation */,
11146
+ reasonDetail: "acknowledgment"
11147
+ };
10578
11148
  }
10579
11149
  if (args.input.isExplicitMention) {
10580
11150
  if (isThreadOptOutInstruction(rawText, text)) {
@@ -10590,18 +11160,21 @@ async function decideSubscribedThreadReply(args) {
10590
11160
  reason: "explicit_mention" /* ExplicitMention */
10591
11161
  };
10592
11162
  }
11163
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
11164
+ return {
11165
+ shouldReply: false,
11166
+ reason: "side_conversation" /* SideConversation */,
11167
+ reasonDetail: "generic immediate side conversation"
11168
+ };
11169
+ }
10593
11170
  try {
10594
11171
  const result = await args.completeObject({
10595
11172
  modelId: args.modelId,
10596
11173
  schema: replyDecisionSchema,
10597
11174
  maxTokens: 120,
10598
11175
  temperature: 0,
10599
- system: buildRouterSystemPrompt(
10600
- args.botUserName,
10601
- args.input.conversationContext,
10602
- args.input.isExplicitMention
10603
- ),
10604
- prompt: rawText,
11176
+ system: buildRouterSystemPrompt(args.botUserName),
11177
+ prompt: buildRouterPrompt(rawText, signals),
10605
11178
  metadata: {
10606
11179
  modelId: args.modelId,
10607
11180
  threadId: args.input.context.threadId ?? "",
@@ -10612,6 +11185,7 @@ async function decideSubscribedThreadReply(args) {
10612
11185
  });
10613
11186
  const parsed = replyDecisionSchema.parse(result.object);
10614
11187
  const reason = parsed.reason?.trim() || "classifier";
11188
+ const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
10615
11189
  if (parsed.should_unsubscribe) {
10616
11190
  if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
10617
11191
  return {
@@ -10634,7 +11208,7 @@ async function decideSubscribedThreadReply(args) {
10634
11208
  reasonDetail: reason
10635
11209
  };
10636
11210
  }
10637
- if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
11211
+ if (parsed.confidence < replyConfidenceThreshold) {
10638
11212
  return {
10639
11213
  shouldReply: false,
10640
11214
  reason: "low_confidence" /* LowConfidence */,
@@ -11229,16 +11803,166 @@ var MAX_USER_ATTACHMENTS = 3;
11229
11803
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
11230
11804
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
11231
11805
  var MAX_VISION_SUMMARY_CHARS = 500;
11232
- async function resolveUserAttachments(attachments, context) {
11806
+ function isVisionEnabled() {
11807
+ return Boolean(botConfig.visionModelId);
11808
+ }
11809
+ var ImageAttachmentProcessingError = class extends Error {
11810
+ constructor(message) {
11811
+ super(message);
11812
+ this.name = "ImageAttachmentProcessingError";
11813
+ }
11814
+ };
11815
+ function buildImageAttachmentPromptText(args) {
11816
+ return [
11817
+ "<image-attachment>",
11818
+ `filename: ${args.filename ?? "unnamed"}`,
11819
+ `media_type: ${args.mediaType}`,
11820
+ "<summary>",
11821
+ args.summary,
11822
+ "</summary>",
11823
+ "</image-attachment>"
11824
+ ].join("\n");
11825
+ }
11826
+ async function summarizeImageWithVision(args) {
11827
+ const visionModelId = botConfig.visionModelId;
11828
+ if (!visionModelId) {
11829
+ return void 0;
11830
+ }
11831
+ const result = await args.completeText({
11832
+ modelId: visionModelId,
11833
+ temperature: 0,
11834
+ maxTokens: args.maxTokens,
11835
+ messages: [
11836
+ {
11837
+ role: "user",
11838
+ content: [
11839
+ {
11840
+ type: "text",
11841
+ text: args.prompt
11842
+ },
11843
+ {
11844
+ type: "image",
11845
+ data: args.imageData.toString("base64"),
11846
+ mimeType: args.mimeType
11847
+ }
11848
+ ],
11849
+ timestamp: Date.now()
11850
+ }
11851
+ ],
11852
+ metadata: {
11853
+ modelId: visionModelId,
11854
+ ...args.metadata
11855
+ }
11856
+ });
11857
+ const summary = result.text.trim().replace(/\s+/g, " ");
11858
+ return summary || void 0;
11859
+ }
11860
+ function truncateVisionSummary(summary) {
11861
+ return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
11862
+ }
11863
+ function getCachedImageSummaries(args) {
11864
+ if (!args.conversation || !args.messageTs) {
11865
+ return [];
11866
+ }
11867
+ const conversationMessage = args.conversation.messages.find(
11868
+ (message) => getConversationMessageSlackTs(message) === args.messageTs
11869
+ );
11870
+ if (!conversationMessage) {
11871
+ return [];
11872
+ }
11873
+ return (conversationMessage.meta?.imageFileIds ?? []).map(
11874
+ (fileId) => args.conversation?.vision.byFileId[fileId]?.summary?.trim()
11875
+ );
11876
+ }
11877
+ function createImageAttachmentProcessingError(attachment) {
11878
+ const label = attachment.filename ? `"${attachment.filename}"` : "this image";
11879
+ return new ImageAttachmentProcessingError(
11880
+ `Image attachment ${label} could not be analyzed`
11881
+ );
11882
+ }
11883
+ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
11233
11884
  if (!attachments || attachments.length === 0) {
11234
11885
  return [];
11235
11886
  }
11236
11887
  const results = [];
11888
+ const cachedImageSummaries = getCachedImageSummaries({
11889
+ conversation: context.conversation,
11890
+ messageTs: context.messageTs
11891
+ });
11892
+ let nextCachedImageSummaryIndex = 0;
11237
11893
  for (const attachment of attachments) {
11238
11894
  if (results.length >= MAX_USER_ATTACHMENTS) break;
11239
11895
  if (attachment.type !== "image" && attachment.type !== "file") continue;
11240
11896
  const mediaType = attachment.mimeType ?? "application/octet-stream";
11897
+ const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
11898
+ if (isImageAttachment && !isVisionEnabled()) {
11899
+ continue;
11900
+ }
11241
11901
  try {
11902
+ const resolvedAttachment = {
11903
+ mediaType,
11904
+ filename: attachment.name
11905
+ };
11906
+ if (isImageAttachment) {
11907
+ const cachedSummary = cachedImageSummaries[nextCachedImageSummaryIndex];
11908
+ nextCachedImageSummaryIndex += 1;
11909
+ if (cachedSummary) {
11910
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
11911
+ filename: attachment.name,
11912
+ mediaType,
11913
+ summary: cachedSummary
11914
+ });
11915
+ results.push(resolvedAttachment);
11916
+ continue;
11917
+ }
11918
+ let imageData = null;
11919
+ if (attachment.fetchData) {
11920
+ imageData = await attachment.fetchData();
11921
+ } else if (attachment.data instanceof Buffer) {
11922
+ imageData = attachment.data;
11923
+ }
11924
+ if (!imageData) {
11925
+ throw createImageAttachmentProcessingError({
11926
+ filename: attachment.name
11927
+ });
11928
+ }
11929
+ if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
11930
+ throw createImageAttachmentProcessingError({
11931
+ filename: attachment.name
11932
+ });
11933
+ }
11934
+ const summary = await summarizeImageWithVision({
11935
+ completeText: deps.completeText,
11936
+ imageData,
11937
+ mimeType: mediaType,
11938
+ maxTokens: 220,
11939
+ prompt: [
11940
+ "Extract concise, factual context from this user-provided image.",
11941
+ "Focus on visible text, UI state, charts, diagrams, errors, names, and other concrete details useful for answering the user's current request.",
11942
+ "Do not speculate.",
11943
+ "Return plain text only."
11944
+ ].join(" "),
11945
+ metadata: {
11946
+ threadId: context.threadId ?? "",
11947
+ channelId: context.channelId ?? "",
11948
+ requesterId: context.requesterId ?? "",
11949
+ runId: context.runId ?? "",
11950
+ filename: attachment.name ?? ""
11951
+ }
11952
+ });
11953
+ if (!summary) {
11954
+ throw createImageAttachmentProcessingError({
11955
+ filename: attachment.name
11956
+ });
11957
+ }
11958
+ resolvedAttachment.promptText = buildImageAttachmentPromptText({
11959
+ filename: attachment.name,
11960
+ mediaType,
11961
+ summary: truncateVisionSummary(summary)
11962
+ });
11963
+ results.push(resolvedAttachment);
11964
+ continue;
11965
+ }
11242
11966
  let data = null;
11243
11967
  if (attachment.fetchData) {
11244
11968
  data = await attachment.fetchData();
@@ -11265,12 +11989,32 @@ async function resolveUserAttachments(attachments, context) {
11265
11989
  );
11266
11990
  continue;
11267
11991
  }
11268
- results.push({
11269
- data,
11270
- mediaType,
11271
- filename: attachment.name
11272
- });
11992
+ resolvedAttachment.data = data;
11993
+ results.push(resolvedAttachment);
11273
11994
  } catch (error) {
11995
+ if (isImageAttachment) {
11996
+ const attachmentError = error instanceof ImageAttachmentProcessingError ? error : createImageAttachmentProcessingError({
11997
+ filename: attachment.name
11998
+ });
11999
+ logWarn(
12000
+ "image_attachment_processing_failed",
12001
+ {
12002
+ slackThreadId: context.threadId,
12003
+ slackUserId: context.requesterId,
12004
+ slackChannelId: context.channelId,
12005
+ runId: context.runId,
12006
+ assistantUserName: botConfig.userName,
12007
+ modelId: botConfig.visionModelId ?? botConfig.modelId
12008
+ },
12009
+ {
12010
+ "error.message": error instanceof Error ? error.message : String(error),
12011
+ "file.mime_type": mediaType,
12012
+ ...attachment.name ? { "file.name": attachment.name } : {}
12013
+ },
12014
+ "Image attachment processing failed"
12015
+ );
12016
+ throw attachmentError;
12017
+ }
11274
12018
  logWarn(
11275
12019
  "attachment_resolution_failed",
11276
12020
  {
@@ -11292,35 +12036,23 @@ async function resolveUserAttachments(attachments, context) {
11292
12036
  return results;
11293
12037
  }
11294
12038
  async function summarizeConversationImage(args, deps) {
12039
+ const visionModelId = botConfig.visionModelId;
12040
+ if (!visionModelId) {
12041
+ return void 0;
12042
+ }
11295
12043
  try {
11296
- const result = await deps.completeText({
11297
- modelId: botConfig.modelId,
11298
- temperature: 0,
12044
+ const summary = await summarizeImageWithVision({
12045
+ completeText: deps.completeText,
12046
+ imageData: args.imageData,
12047
+ mimeType: args.mimeType,
11299
12048
  maxTokens: 220,
11300
- messages: [
11301
- {
11302
- role: "user",
11303
- content: [
11304
- {
11305
- type: "text",
11306
- text: [
11307
- "Extract concise, factual context from this image for future thread turns.",
11308
- "Focus on visible text, names, titles, companies, and candidate-identifying details.",
11309
- "Do not speculate.",
11310
- "Return plain text only."
11311
- ].join(" ")
11312
- },
11313
- {
11314
- type: "image",
11315
- data: args.imageData.toString("base64"),
11316
- mimeType: args.mimeType
11317
- }
11318
- ],
11319
- timestamp: Date.now()
11320
- }
11321
- ],
12049
+ prompt: [
12050
+ "Extract concise, factual context from this image for future thread turns.",
12051
+ "Focus on visible text, names, titles, companies, and candidate-identifying details.",
12052
+ "Do not speculate.",
12053
+ "Return plain text only."
12054
+ ].join(" "),
11322
12055
  metadata: {
11323
- modelId: botConfig.modelId,
11324
12056
  threadId: args.context.threadId ?? "",
11325
12057
  channelId: args.context.channelId ?? "",
11326
12058
  requesterId: args.context.requesterId ?? "",
@@ -11328,11 +12060,10 @@ async function summarizeConversationImage(args, deps) {
11328
12060
  fileId: args.fileId
11329
12061
  }
11330
12062
  });
11331
- const summary = result.text.trim().replace(/\s+/g, " ");
11332
12063
  if (!summary) {
11333
12064
  return void 0;
11334
12065
  }
11335
- return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
12066
+ return truncateVisionSummary(summary);
11336
12067
  } catch (error) {
11337
12068
  logWarn(
11338
12069
  "conversation_image_vision_failed",
@@ -11342,7 +12073,7 @@ async function summarizeConversationImage(args, deps) {
11342
12073
  slackChannelId: args.context.channelId,
11343
12074
  runId: args.context.runId,
11344
12075
  assistantUserName: botConfig.userName,
11345
- modelId: botConfig.modelId
12076
+ modelId: visionModelId
11346
12077
  },
11347
12078
  {
11348
12079
  "error.message": error instanceof Error ? error.message : String(error),
@@ -11355,6 +12086,9 @@ async function summarizeConversationImage(args, deps) {
11355
12086
  }
11356
12087
  }
11357
12088
  async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
12089
+ if (!isVisionEnabled()) {
12090
+ return;
12091
+ }
11358
12092
  if (!context.channelId || !context.threadTs) {
11359
12093
  return;
11360
12094
  }
@@ -11555,6 +12289,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
11555
12289
  }
11556
12290
  function createVisionContextService(deps) {
11557
12291
  return {
12292
+ resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
11558
12293
  hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
11559
12294
  conversation,
11560
12295
  context,
@@ -11593,111 +12328,6 @@ function createJuniorRuntimeServices(overrides = {}) {
11593
12328
  };
11594
12329
  }
11595
12330
 
11596
- // src/chat/runtime/progress-reporter.ts
11597
- var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
11598
- var STATUS_MIN_VISIBLE_MS = 1200;
11599
- function createProgressReporter(args) {
11600
- const now = args.now ?? (() => Date.now());
11601
- const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
11602
- const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
11603
- let active = false;
11604
- let currentStatus = "";
11605
- let lastStatusAt = 0;
11606
- let pendingStatus = null;
11607
- let pendingTimer = null;
11608
- let inflightStatusUpdate = Promise.resolve();
11609
- const postStatus = async (text) => {
11610
- const channelId = args.channelId;
11611
- const threadTs = args.threadTs;
11612
- if (!channelId || !threadTs) {
11613
- return;
11614
- }
11615
- if (!text && !currentStatus) {
11616
- return;
11617
- }
11618
- currentStatus = text;
11619
- lastStatusAt = now();
11620
- const suggestions = text ? [text] : void 0;
11621
- const previous = inflightStatusUpdate;
11622
- const request = (async () => {
11623
- await previous;
11624
- try {
11625
- await args.setAssistantStatus(channelId, threadTs, text, suggestions);
11626
- } catch (error) {
11627
- logWarn(
11628
- "assistant_status_update_failed",
11629
- {},
11630
- {
11631
- "app.slack.status_text": text || "(clear)",
11632
- "error.message": error instanceof Error ? error.message : String(error)
11633
- },
11634
- "Failed to update assistant status"
11635
- );
11636
- }
11637
- })();
11638
- inflightStatusUpdate = request;
11639
- await request;
11640
- };
11641
- const clearPending = () => {
11642
- if (pendingTimer) {
11643
- clearTimer(pendingTimer);
11644
- pendingTimer = null;
11645
- }
11646
- pendingStatus = null;
11647
- };
11648
- const flushPending = async () => {
11649
- if (!active || !pendingStatus) {
11650
- clearPending();
11651
- return;
11652
- }
11653
- const next = pendingStatus;
11654
- clearPending();
11655
- if (next !== currentStatus) {
11656
- await postStatus(next);
11657
- }
11658
- };
11659
- return {
11660
- async start() {
11661
- active = true;
11662
- clearPending();
11663
- void postStatus("Thinking...");
11664
- },
11665
- async stop() {
11666
- active = false;
11667
- clearPending();
11668
- await postStatus("");
11669
- },
11670
- async setStatus(text) {
11671
- const truncated = truncateStatusText(text);
11672
- if (!active || !truncated || truncated === currentStatus || truncated === pendingStatus) {
11673
- return;
11674
- }
11675
- const elapsed = now() - lastStatusAt;
11676
- const waitMs = Math.max(
11677
- STATUS_UPDATE_DEBOUNCE_MS - elapsed,
11678
- STATUS_MIN_VISIBLE_MS - elapsed,
11679
- 0
11680
- );
11681
- if (waitMs <= 0) {
11682
- clearPending();
11683
- void postStatus(truncated);
11684
- return;
11685
- }
11686
- pendingStatus = truncated;
11687
- if (pendingTimer) {
11688
- return;
11689
- }
11690
- pendingTimer = setTimer(
11691
- () => {
11692
- pendingTimer = null;
11693
- void flushPending();
11694
- },
11695
- Math.max(1, waitMs)
11696
- );
11697
- }
11698
- };
11699
- }
11700
-
11701
12331
  // src/chat/runtime/streaming.ts
11702
12332
  function createTextStreamBridge() {
11703
12333
  const queue = [];
@@ -11738,36 +12368,6 @@ function createTextStreamBridge() {
11738
12368
  }
11739
12369
  };
11740
12370
  }
11741
- function createNormalizingStream(inner, normalize) {
11742
- return {
11743
- async *[Symbol.asyncIterator]() {
11744
- let accumulated = "";
11745
- let emitted = 0;
11746
- for await (const chunk of inner) {
11747
- accumulated += chunk;
11748
- const lastNewline = accumulated.lastIndexOf("\n");
11749
- if (lastNewline === -1) {
11750
- const delta2 = accumulated.slice(emitted);
11751
- if (delta2) {
11752
- yield delta2;
11753
- emitted = accumulated.length;
11754
- }
11755
- continue;
11756
- }
11757
- const stable = accumulated.slice(0, lastNewline + 1);
11758
- const normalized = normalize(stable);
11759
- const delta = normalized.slice(emitted);
11760
- emitted = normalized.length;
11761
- if (delta) yield delta;
11762
- }
11763
- if (accumulated) {
11764
- const normalized = normalize(accumulated);
11765
- const delta = normalized.slice(emitted);
11766
- if (delta) yield delta;
11767
- }
11768
- }
11769
- };
11770
- }
11771
12371
 
11772
12372
  // src/chat/runtime/reply-executor.ts
11773
12373
  function getExecutionFailureReason(reply) {
@@ -11869,19 +12469,23 @@ function createReplyToThread(deps) {
11869
12469
  if (resolvedUserName) {
11870
12470
  setTags({ slackUserName: resolvedUserName });
11871
12471
  }
11872
- const userAttachments = await resolveUserAttachments(
12472
+ const userAttachments = await deps.resolveUserAttachments(
11873
12473
  message.attachments,
11874
12474
  {
11875
12475
  threadId,
11876
12476
  requesterId: message.author.userId,
11877
12477
  channelId,
11878
- runId
12478
+ runId,
12479
+ conversation: preparedState.conversation,
12480
+ messageTs: message.id
11879
12481
  }
11880
12482
  );
11881
12483
  const progress = createProgressReporter({
11882
12484
  channelId,
11883
12485
  threadTs,
11884
- setAssistantStatus: (channel, thread2, text, suggestions) => deps.getSlackAdapter().setAssistantStatus(channel, thread2, text, suggestions)
12486
+ transport: createSlackAdapterAssistantStatusTransport({
12487
+ getSlackAdapter: deps.getSlackAdapter
12488
+ })
11885
12489
  });
11886
12490
  const textStream = createTextStreamBridge();
11887
12491
  let streamedReplyPromise;
@@ -11898,10 +12502,7 @@ function createReplyToThread(deps) {
11898
12502
  if (!streamedReplyPromise) {
11899
12503
  const streamingReply = (async () => {
11900
12504
  return await postThreadReply(
11901
- createNormalizingStream(
11902
- textStream.iterable,
11903
- ensureBlockSpacing
11904
- ),
12505
+ textStream.iterable,
11905
12506
  "streaming_initial_post"
11906
12507
  );
11907
12508
  })();
@@ -12286,7 +12887,7 @@ function createPrepareTurnState(deps) {
12286
12887
  conversation,
12287
12888
  incomingUserMessage
12288
12889
  );
12289
- if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
12890
+ if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
12290
12891
  await deps.hydrateConversationVisionContext(conversation, {
12291
12892
  threadId: args.context.threadId,
12292
12893
  channelId: args.context.channelId,
@@ -12333,6 +12934,7 @@ function createSlackRuntime(options) {
12333
12934
  const replyToThread = createReplyToThread({
12334
12935
  getSlackAdapter: options.getSlackAdapter,
12335
12936
  prepareTurnState,
12937
+ resolveUserAttachments: services.visionContext.resolveUserAttachments,
12336
12938
  services: services.replyExecutor
12337
12939
  });
12338
12940
  return createSlackTurnRuntime({