@sentry/junior 0.3.0 → 0.4.1

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.
@@ -10,6 +10,7 @@ import {
10
10
  getPluginOAuthConfig,
11
11
  getPluginProviders,
12
12
  getPluginSkillRoots,
13
+ getRuntimeDependencyProfileHash,
13
14
  getSlackBotToken,
14
15
  getSlackClientId,
15
16
  getSlackClientSecret,
@@ -25,7 +26,7 @@ import {
25
26
  skillRoots,
26
27
  soulPathCandidates,
27
28
  upsertAgentTurnSessionCheckpoint
28
- } from "./chunk-DPTR2FNH.js";
29
+ } from "./chunk-OZFXD5IG.js";
29
30
  import {
30
31
  logError,
31
32
  logException,
@@ -656,22 +657,34 @@ function normalizeForSlack(text) {
656
657
  normalized = ensureBlockSpacing(normalized);
657
658
  return normalized.replace(/\n{3,}/g, "\n\n").trim();
658
659
  }
659
- function buildSlackOutputMessage(text, options = {}) {
660
+ function buildSlackOutputMessage(text, files) {
660
661
  const normalized = normalizeForSlack(text);
662
+ const fileCount = files?.length ?? 0;
661
663
  if (!normalized) {
662
- logWarn("slack_output_normalized_empty", {}, {
663
- "app.output.original_length": text.length,
664
- "app.output.parsed_length": normalized.length,
665
- "app.output.file_count": options.files?.length ?? 0
666
- }, "Slack output normalized to empty content");
664
+ if (fileCount > 0) {
665
+ return {
666
+ raw: "",
667
+ files
668
+ };
669
+ }
670
+ logWarn(
671
+ "slack_output_normalized_empty",
672
+ {},
673
+ {
674
+ "app.output.original_length": text.length,
675
+ "app.output.parsed_length": normalized.length,
676
+ "app.output.file_count": fileCount
677
+ },
678
+ "Slack output normalized to empty content"
679
+ );
667
680
  return {
668
681
  markdown: "I couldn't produce a response.",
669
- files: options.files
682
+ files
670
683
  };
671
684
  }
672
685
  return {
673
686
  markdown: normalized,
674
- files: options.files
687
+ files
675
688
  };
676
689
  }
677
690
  var slackOutputPolicy = {
@@ -759,10 +772,14 @@ function formatAvailableSkillsForPrompt(skills) {
759
772
  const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
760
773
  lines.push(" <skill>");
761
774
  lines.push(` <name>${escapeXml(skill.name)}</name>`);
762
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
775
+ lines.push(
776
+ ` <description>${escapeXml(skill.description)}</description>`
777
+ );
763
778
  lines.push(` <location>${escapeXml(skillLocation)}</location>`);
764
779
  if (skill.usesConfig && skill.usesConfig.length > 0) {
765
- lines.push(` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`);
780
+ lines.push(
781
+ ` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
782
+ );
766
783
  }
767
784
  lines.push(" </skill>");
768
785
  }
@@ -776,10 +793,14 @@ function formatLoadedSkillsForPrompt(skills) {
776
793
  const lines = ["<loaded_skills>"];
777
794
  for (const skill of skills) {
778
795
  const skillDir = workspaceSkillDir(skill.name);
779
- lines.push(` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`);
796
+ lines.push(
797
+ ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
798
+ );
780
799
  lines.push(`References are relative to ${escapeXml(skillDir)}.`);
781
800
  if (skill.usesConfig && skill.usesConfig.length > 0) {
782
- lines.push(`Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`);
801
+ lines.push(
802
+ `Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
803
+ );
783
804
  }
784
805
  lines.push("");
785
806
  lines.push(skill.body);
@@ -859,12 +880,20 @@ function buildSystemPrompt(params) {
859
880
  "Loaded skills for this turn:",
860
881
  formatLoadedSkillsForPrompt(activeSkills)
861
882
  ].join("\n");
862
- const configurationKeys = Object.keys(configuration ?? {}).sort((a, b) => a.localeCompare(b));
883
+ const configurationKeys = Object.keys(configuration ?? {}).sort(
884
+ (a, b) => a.localeCompare(b)
885
+ );
863
886
  const relevantConfigSet = new Set(
864
- (relevantConfigurationKeys ?? []).filter((key) => Object.prototype.hasOwnProperty.call(configuration ?? {}, key))
887
+ (relevantConfigurationKeys ?? []).filter(
888
+ (key) => Object.prototype.hasOwnProperty.call(configuration ?? {}, key)
889
+ )
890
+ );
891
+ const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map(
892
+ (key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
893
+ );
894
+ const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map(
895
+ (key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
865
896
  );
866
- const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map((key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`);
867
- const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map((key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`);
868
897
  const configurationSection = [
869
898
  "Use these conversation-scoped defaults when the user has not provided explicit values in this turn.",
870
899
  "If explicit user input conflicts with configuration, follow explicit user input.",
@@ -936,6 +965,10 @@ function buildSystemPrompt(params) {
936
965
  "- For factual or external questions, run tools/skills first, then answer from evidence.",
937
966
  "- Use tool descriptions as the source of truth for when each tool should or should not be called.",
938
967
  "- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
968
+ "- Use `attachFile` to attach files from the sandbox (for example screenshots, PDFs, logs) to the Slack reply.",
969
+ "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
970
+ "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
971
+ "- If `attachFile` fails, explain the failure and do not say the file was shared.",
939
972
  "- Use `imageGenerate` when the user asks for image creation.",
940
973
  "- Use `slackCanvasCreate` for long-form docs/specs and `slackCanvasUpdate` for doc follow-ups.",
941
974
  "- `slackCanvasUpdate` targets the active artifact-context canvas automatically; do not ask the user for `canvas_id`.",
@@ -946,7 +979,7 @@ function buildSystemPrompt(params) {
946
979
  "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
947
980
  "- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, not unicode emoji), and avoid redundant acknowledgment text.",
948
981
  "- Suggested acknowledgement reactions include \u{1F44B}, \u2705, \u{1F44D}, and \u{1F440}, but choose what best fits the request.",
949
- "- To enable provider credentials for this turn, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before commands that need authenticated API calls.",
982
+ "- To enable provider credentials for this turn, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before commands that need authenticated API calls. GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
950
983
  "- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
951
984
  "- Capabilities are provider-qualified (for example `github.issues.write`).",
952
985
  "- When your work is complete, provide the exact user-facing markdown response.",
@@ -958,10 +991,10 @@ function buildSystemPrompt(params) {
958
991
  renderTag(
959
992
  "skills",
960
993
  [
961
- "- For explicit slash commands, treat `/skill-name` as authoritative intent for that skill.",
962
- "- If slash-invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
963
- "- Otherwise, for slash-invoked skills, call `loadSkill` for that exact skill before applying skill-specific behavior.",
964
- "- For non-slash requests where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
994
+ "- Explicit skill triggers may appear as `/skillname` or `!skillname`.",
995
+ "- If explicitly invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
996
+ "- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
997
+ "- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
965
998
  "- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
966
999
  "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
967
1000
  "- Load only the best matching skill first; do not load multiple skills upfront.",
@@ -987,7 +1020,7 @@ function buildSystemPrompt(params) {
987
1020
  activeSkillsSection,
988
1021
  renderTag(
989
1022
  "invocation-context",
990
- invocation ? `Slash invocation detected: /${invocation.skillName}` : "No slash invocation detected."
1023
+ invocation ? invocation.source === "hard_bang" ? `Explicit skill trigger detected: !${invocation.skillName}` : `Legacy slash hint detected: /${invocation.skillName} (non-authoritative)` : "No explicit skill trigger detected."
991
1024
  )
992
1025
  ];
993
1026
  return sections.join("\n\n");
@@ -1278,6 +1311,7 @@ var SkillCapabilityRuntime = class {
1278
1311
  // src/chat/credentials/state-adapter-token-store.ts
1279
1312
  var KEY_PREFIX = "oauth-token";
1280
1313
  var BUFFER_MS = 24 * 60 * 60 * 1e3;
1314
+ var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
1281
1315
  function tokenKey(userId, provider) {
1282
1316
  return `${KEY_PREFIX}:${userId}:${provider}`;
1283
1317
  }
@@ -1287,11 +1321,13 @@ var StateAdapterTokenStore = class {
1287
1321
  this.state = stateAdapter;
1288
1322
  }
1289
1323
  async get(userId, provider) {
1290
- const stored = await this.state.get(tokenKey(userId, provider));
1324
+ const stored = await this.state.get(
1325
+ tokenKey(userId, provider)
1326
+ );
1291
1327
  return stored ?? void 0;
1292
1328
  }
1293
1329
  async set(userId, provider, tokens) {
1294
- const ttlMs = Math.max(tokens.expiresAt - Date.now() + BUFFER_MS, BUFFER_MS);
1330
+ const ttlMs = tokens.expiresAt ? Math.max(tokens.expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
1295
1331
  await this.state.set(tokenKey(userId, provider), tokens, ttlMs);
1296
1332
  }
1297
1333
  async delete(userId, provider) {
@@ -1319,6 +1355,7 @@ var TestCredentialBroker = class {
1319
1355
  headerTransforms: this.config.domains.map((domain) => ({
1320
1356
  domain,
1321
1357
  headers: {
1358
+ ...this.config.apiHeaders ?? {},
1322
1359
  Authorization: `Bearer ${token}`
1323
1360
  }
1324
1361
  })),
@@ -1350,7 +1387,13 @@ function createSkillCapabilityRuntime(options = {}) {
1350
1387
  continue;
1351
1388
  }
1352
1389
  const placeholder = resolveAuthTokenPlaceholder(credentials);
1353
- brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({ provider: name, domains: credentials.apiDomains, envKey: credentials.authTokenEnv, placeholder }) : createPluginBroker(name, { userTokenStore });
1390
+ brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
1391
+ provider: name,
1392
+ domains: credentials.apiDomains,
1393
+ apiHeaders: credentials.apiHeaders,
1394
+ envKey: credentials.authTokenEnv,
1395
+ placeholder
1396
+ }) : createPluginBroker(name, { userTokenStore });
1354
1397
  }
1355
1398
  const router = new ProviderCredentialRouter({ brokersByProvider });
1356
1399
  return new SkillCapabilityRuntime({
@@ -1362,9 +1405,11 @@ function createSkillCapabilityRuntime(options = {}) {
1362
1405
  }
1363
1406
 
1364
1407
  // src/chat/capabilities/jr-rpc-command.ts
1365
- import { randomBytes } from "crypto";
1366
1408
  import { Bash, defineCommand } from "just-bash";
1367
1409
 
1410
+ // src/chat/oauth-flow.ts
1411
+ import { randomBytes } from "crypto";
1412
+
1368
1413
  // src/chat/slack-actions/client.ts
1369
1414
  import { WebClient } from "@slack/web-api";
1370
1415
  var SlackActionError = class extends Error {
@@ -1620,19 +1665,36 @@ async function downloadPrivateSlackFile(url) {
1620
1665
  return Buffer.from(await response.arrayBuffer());
1621
1666
  }
1622
1667
 
1623
- // src/chat/capabilities/jr-rpc-command.ts
1668
+ // src/chat/oauth-flow.ts
1669
+ var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
1670
+ function formatProviderLabel(provider) {
1671
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
1672
+ }
1673
+ function resolveBaseUrl() {
1674
+ const explicit = process.env.JUNIOR_BASE_URL?.trim();
1675
+ if (explicit) return explicit;
1676
+ const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
1677
+ if (vercelProd) return `https://${vercelProd}`;
1678
+ const vercelUrl = process.env.VERCEL_URL?.trim();
1679
+ if (vercelUrl) return `https://${vercelUrl}`;
1680
+ return void 0;
1681
+ }
1624
1682
  async function deliverPrivateMessage(input) {
1625
1683
  let client2;
1626
1684
  try {
1627
1685
  client2 = getSlackClient();
1628
1686
  } catch {
1629
- logWarn("oauth_private_delivery_skip", {}, { "app.reason": "missing_bot_token" }, "Skipped private message delivery \u2014 no SLACK_BOT_TOKEN");
1687
+ logWarn(
1688
+ "oauth_private_delivery_skip",
1689
+ {},
1690
+ { "app.reason": "missing_bot_token" },
1691
+ "Skipped private message delivery \u2014 no SLACK_BOT_TOKEN"
1692
+ );
1630
1693
  return false;
1631
1694
  }
1632
1695
  if (input.channelId) {
1633
- const isDm = isDmChannel(input.channelId);
1634
1696
  try {
1635
- if (isDm) {
1697
+ if (isDmChannel(input.channelId)) {
1636
1698
  await client2.chat.postMessage({
1637
1699
  channel: input.channelId,
1638
1700
  text: input.text,
@@ -1648,35 +1710,113 @@ async function deliverPrivateMessage(input) {
1648
1710
  }
1649
1711
  return "in_context";
1650
1712
  } catch (error) {
1651
- const slackError = error instanceof Error ? error.message : String(error);
1652
1713
  logWarn(
1653
1714
  "oauth_private_delivery_failed",
1654
1715
  {},
1655
- { "app.slack.error": slackError, "app.slack.channel": input.channelId },
1656
- `${isDm ? "DM" : "Ephemeral"} message delivery failed, falling back to DM`
1716
+ {
1717
+ "app.slack.error": error instanceof Error ? error.message : String(error),
1718
+ "app.slack.channel": input.channelId
1719
+ },
1720
+ "Private message delivery failed, falling back to DM"
1657
1721
  );
1658
1722
  }
1659
1723
  }
1660
1724
  try {
1661
- const openResult = await client2.conversations.open({ users: input.userId });
1662
- const dmChannelId = openResult.channel?.id;
1725
+ const dmChannelId = (await client2.conversations.open({ users: input.userId })).channel?.id;
1663
1726
  if (!dmChannelId) {
1664
- logWarn("oauth_dm_fallback_failed", {}, { "app.reason": "no_dm_channel_id" }, "conversations.open returned no channel ID");
1727
+ logWarn(
1728
+ "oauth_dm_fallback_failed",
1729
+ {},
1730
+ { "app.reason": "no_dm_channel_id" },
1731
+ "conversations.open returned no channel ID"
1732
+ );
1665
1733
  return false;
1666
1734
  }
1667
1735
  await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
1668
1736
  return "fallback_dm";
1669
1737
  } catch (error) {
1670
- const slackError = error instanceof Error ? error.message : String(error);
1671
1738
  logWarn(
1672
1739
  "oauth_dm_fallback_failed",
1673
1740
  {},
1674
- { "app.slack.error": slackError },
1741
+ {
1742
+ "app.slack.error": error instanceof Error ? error.message : String(error)
1743
+ },
1675
1744
  "DM fallback delivery failed"
1676
1745
  );
1677
1746
  return false;
1678
1747
  }
1679
1748
  }
1749
+ async function startOAuthFlow(provider, input) {
1750
+ const providerConfig = getPluginOAuthConfig(provider);
1751
+ if (!providerConfig) {
1752
+ return {
1753
+ ok: false,
1754
+ error: `Provider "${provider}" does not support OAuth authorization`
1755
+ };
1756
+ }
1757
+ const clientId = process.env[providerConfig.clientIdEnv]?.trim();
1758
+ if (!clientId) {
1759
+ return {
1760
+ ok: false,
1761
+ error: `Missing ${providerConfig.clientIdEnv} environment variable`
1762
+ };
1763
+ }
1764
+ const baseUrl = resolveBaseUrl();
1765
+ if (!baseUrl) {
1766
+ return {
1767
+ ok: false,
1768
+ error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
1769
+ };
1770
+ }
1771
+ const configuration = input.userMessage && input.channelConfiguration ? await input.channelConfiguration.resolveValues() : void 0;
1772
+ const state = randomBytes(32).toString("hex");
1773
+ await getStateAdapter().set(
1774
+ `oauth-state:${state}`,
1775
+ {
1776
+ userId: input.requesterId,
1777
+ provider,
1778
+ ...input.channelId ? { channelId: input.channelId } : {},
1779
+ ...input.threadTs ? { threadTs: input.threadTs } : {},
1780
+ ...input.userMessage ? { pendingMessage: input.userMessage } : {},
1781
+ ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
1782
+ },
1783
+ OAUTH_STATE_TTL_MS
1784
+ );
1785
+ const authorizeParams = new URLSearchParams({
1786
+ client_id: clientId,
1787
+ state,
1788
+ redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
1789
+ response_type: "code"
1790
+ });
1791
+ if (providerConfig.scope) {
1792
+ authorizeParams.set("scope", providerConfig.scope);
1793
+ }
1794
+ for (const [key, value] of Object.entries(
1795
+ providerConfig.authorizeParams ?? {}
1796
+ )) {
1797
+ authorizeParams.set(key, value);
1798
+ }
1799
+ logInfo(
1800
+ "jr_rpc_oauth_start",
1801
+ {},
1802
+ {
1803
+ "app.credential.provider": provider,
1804
+ ...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : {}
1805
+ },
1806
+ "Initiated OAuth authorization code flow"
1807
+ );
1808
+ return {
1809
+ ok: true,
1810
+ delivery: await deliverPrivateMessage({
1811
+ channelId: input.channelId,
1812
+ threadTs: input.threadTs,
1813
+ userId: input.requesterId,
1814
+ text: `<${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}|Click here to link your ${formatProviderLabel(provider)} account>. Once you've authorized, you'll see a confirmation in Slack.`
1815
+ })
1816
+ };
1817
+ }
1818
+
1819
+ // src/chat/capabilities/jr-rpc-command.ts
1680
1820
  function commandResult(input) {
1681
1821
  let stdout = "";
1682
1822
  if (typeof input.stdout === "string") {
@@ -1759,7 +1899,7 @@ async function handleIssueCredentialCommand(args, deps) {
1759
1899
  reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
1760
1900
  });
1761
1901
  } catch (error) {
1762
- if (error instanceof CredentialUnavailableError && getOAuthProviderConfig(error.provider) && deps.requesterId) {
1902
+ if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
1763
1903
  const oauthResult = await startOAuthFlow(error.provider, {
1764
1904
  requesterId: deps.requesterId,
1765
1905
  channelId: deps.channelId,
@@ -1769,14 +1909,14 @@ async function handleIssueCredentialCommand(args, deps) {
1769
1909
  activeSkillName: deps.activeSkill?.name ?? void 0
1770
1910
  });
1771
1911
  if (oauthResult.ok) {
1772
- const providerLabel2 = error.provider.charAt(0).toUpperCase() + error.provider.slice(1);
1912
+ const providerLabel = formatProviderLabel(error.provider);
1773
1913
  return commandResult({
1774
1914
  stdout: {
1775
1915
  credential_unavailable: true,
1776
1916
  oauth_started: true,
1777
1917
  provider: error.provider,
1778
1918
  private_delivery_sent: !!oauthResult.delivery,
1779
- message: oauthResult.delivery ? `I need to connect your ${providerLabel2} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel2} 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.`
1919
+ message: oauthResult.delivery ? `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.`
1780
1920
  },
1781
1921
  exitCode: 1
1782
1922
  });
@@ -1986,75 +2126,6 @@ ${usage}
1986
2126
  function isKnownProvider(provider) {
1987
2127
  return listCapabilityProviders().some((p) => p.provider === provider);
1988
2128
  }
1989
- function getOAuthProviderConfig(provider) {
1990
- return getPluginOAuthConfig(provider);
1991
- }
1992
- var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
1993
- function resolveBaseUrl() {
1994
- const explicit = process.env.JUNIOR_BASE_URL?.trim();
1995
- if (explicit) return explicit;
1996
- const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
1997
- if (vercelProd) return `https://${vercelProd}`;
1998
- const vercelUrl = process.env.VERCEL_URL?.trim();
1999
- if (vercelUrl) return `https://${vercelUrl}`;
2000
- return void 0;
2001
- }
2002
- async function startOAuthFlow(provider, input) {
2003
- const providerConfig = getOAuthProviderConfig(provider);
2004
- if (!providerConfig) {
2005
- return { ok: false, error: `Provider "${provider}" does not support OAuth authorization` };
2006
- }
2007
- const clientId = process.env[providerConfig.clientIdEnv]?.trim();
2008
- if (!clientId) {
2009
- return { ok: false, error: `Missing ${providerConfig.clientIdEnv} environment variable` };
2010
- }
2011
- const baseUrl = resolveBaseUrl();
2012
- if (!baseUrl) {
2013
- return { ok: false, error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)" };
2014
- }
2015
- let configuration;
2016
- if (input.userMessage && input.channelConfiguration) {
2017
- configuration = await input.channelConfiguration.resolveValues();
2018
- }
2019
- const state = randomBytes(32).toString("hex");
2020
- const stateKey = `oauth-state:${state}`;
2021
- const stateAdapter = getStateAdapter();
2022
- const statePayload = {
2023
- userId: input.requesterId,
2024
- provider,
2025
- ...input.channelId ? { channelId: input.channelId } : {},
2026
- ...input.threadTs ? { threadTs: input.threadTs } : {},
2027
- ...input.userMessage ? { pendingMessage: input.userMessage } : {},
2028
- ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
2029
- };
2030
- await stateAdapter.set(stateKey, statePayload, OAUTH_STATE_TTL_MS);
2031
- const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
2032
- const params = new URLSearchParams({
2033
- client_id: clientId,
2034
- scope: providerConfig.scope,
2035
- state,
2036
- redirect_uri: redirectUri,
2037
- response_type: "code"
2038
- });
2039
- const authorizeUrl = `${providerConfig.authorizeEndpoint}?${params.toString()}`;
2040
- logInfo(
2041
- "jr_rpc_oauth_start",
2042
- {},
2043
- {
2044
- "app.credential.provider": provider,
2045
- ...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : {}
2046
- },
2047
- "Initiated OAuth authorization code flow"
2048
- );
2049
- const providerLabel2 = provider.charAt(0).toUpperCase() + provider.slice(1);
2050
- const delivery = await deliverPrivateMessage({
2051
- channelId: input.channelId,
2052
- threadTs: input.threadTs,
2053
- userId: input.requesterId,
2054
- text: `<${authorizeUrl}|Click here to link your ${providerLabel2} account>. Once you've authorized, you'll see a confirmation in Slack.`
2055
- });
2056
- return { ok: true, delivery };
2057
- }
2058
2129
  async function handleOAuthStartCommand(args, deps) {
2059
2130
  const provider = (args[0] ?? "").trim();
2060
2131
  if (!provider) {
@@ -2071,14 +2142,14 @@ async function handleOAuthStartCommand(args, deps) {
2071
2142
  }
2072
2143
  if (deps.requesterId && deps.userTokenStore) {
2073
2144
  const stored = await deps.userTokenStore.get(deps.requesterId, provider);
2074
- if (stored && stored.expiresAt > Date.now()) {
2075
- const providerLabel2 = provider.charAt(0).toUpperCase() + provider.slice(1);
2145
+ if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now())) {
2146
+ const providerLabel = formatProviderLabel(provider);
2076
2147
  return commandResult({
2077
2148
  stdout: {
2078
2149
  ok: true,
2079
2150
  already_connected: true,
2080
2151
  provider,
2081
- message: `Your ${providerLabel2} account is already connected.`
2152
+ message: `Your ${providerLabel} account is already connected.`
2082
2153
  },
2083
2154
  exitCode: 0
2084
2155
  });
@@ -2233,6 +2304,38 @@ function isExplicitChannelPostIntent(text) {
2233
2304
  }
2234
2305
 
2235
2306
  // src/chat/delivery/plan.ts
2307
+ var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
2308
+ var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
2309
+ var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
2310
+ function normalizeReactionAckText(text) {
2311
+ return text.trim().toLowerCase().replace(/[!.]+$/g, "");
2312
+ }
2313
+ function isRedundantReactionAckText(text) {
2314
+ const trimmed = text.trim();
2315
+ if (!trimmed) {
2316
+ return false;
2317
+ }
2318
+ if (REACTION_ONLY_ACK_RE.test(trimmed)) {
2319
+ return true;
2320
+ }
2321
+ const normalized = normalizeReactionAckText(text);
2322
+ return REDUNDANT_REACTION_ACK_TEXT.includes(
2323
+ normalized
2324
+ );
2325
+ }
2326
+ function isPotentialRedundantReactionAckText(text) {
2327
+ const trimmed = text.trim();
2328
+ if (!trimmed) {
2329
+ return true;
2330
+ }
2331
+ if (REACTION_ONLY_ACK_RE.test(trimmed) || REACTION_ALIAS_PREFIX_RE.test(trimmed)) {
2332
+ return true;
2333
+ }
2334
+ const normalized = normalizeReactionAckText(text);
2335
+ return REDUNDANT_REACTION_ACK_TEXT.some(
2336
+ (candidate) => candidate.startsWith(normalized)
2337
+ );
2338
+ }
2236
2339
  function buildReplyDeliveryPlan(args) {
2237
2340
  const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
2238
2341
  let attachFiles = "none";
@@ -2501,19 +2604,31 @@ async function discoverSkills(options) {
2501
2604
  }
2502
2605
  function parseSkillInvocation(messageText, availableSkills) {
2503
2606
  const trimmed = messageText.trim();
2504
- const match = /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed);
2505
- if (!match) {
2506
- return null;
2507
- }
2508
- const skillName = match[1].toLowerCase();
2509
- if (!availableSkills.some((s) => s.name === skillName)) {
2510
- return null;
2511
- }
2512
- const args = (match[2] ?? "").trim();
2513
- return {
2514
- skillName,
2515
- args
2607
+ const toInvocation = (match, source) => {
2608
+ if (!match) {
2609
+ return null;
2610
+ }
2611
+ const skillName = match[1].toLowerCase();
2612
+ if (!availableSkills.some((skill) => skill.name === skillName)) {
2613
+ return null;
2614
+ }
2615
+ return {
2616
+ skillName,
2617
+ args: (match[2] ?? "").trim(),
2618
+ source
2619
+ };
2516
2620
  };
2621
+ const hardBangInvocation = toInvocation(
2622
+ /(?:^|\s)!([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
2623
+ "hard_bang"
2624
+ );
2625
+ if (hardBangInvocation) {
2626
+ return hardBangInvocation;
2627
+ }
2628
+ return toInvocation(
2629
+ /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
2630
+ "legacy_slash"
2631
+ );
2517
2632
  }
2518
2633
  function findSkillByName(skillName, available) {
2519
2634
  return available.find((skill) => skill.name === skillName) ?? null;
@@ -2736,8 +2851,135 @@ function createBashTool() {
2736
2851
  });
2737
2852
  }
2738
2853
 
2739
- // src/chat/tools/image-generate.ts
2854
+ // src/chat/tools/attach-file.ts
2855
+ import path3 from "path";
2740
2856
  import { Type as Type2 } from "@sinclair/typebox";
2857
+ var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
2858
+ var MIME_BY_EXTENSION = {
2859
+ ".png": "image/png",
2860
+ ".jpg": "image/jpeg",
2861
+ ".jpeg": "image/jpeg",
2862
+ ".gif": "image/gif",
2863
+ ".webp": "image/webp",
2864
+ ".svg": "image/svg+xml",
2865
+ ".pdf": "application/pdf",
2866
+ ".txt": "text/plain",
2867
+ ".md": "text/markdown",
2868
+ ".json": "application/json",
2869
+ ".csv": "text/csv",
2870
+ ".log": "text/plain"
2871
+ };
2872
+ function normalizeSandboxPath(inputPath) {
2873
+ const trimmed = inputPath.trim();
2874
+ if (!trimmed) {
2875
+ throw new Error("path is required");
2876
+ }
2877
+ if (path3.posix.isAbsolute(trimmed)) {
2878
+ return trimmed;
2879
+ }
2880
+ return path3.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
2881
+ }
2882
+ function sanitizeFilename(value, fallbackPath) {
2883
+ const candidate = (value ?? "").trim();
2884
+ if (candidate) {
2885
+ const base = path3.posix.basename(candidate);
2886
+ if (base && base !== "." && base !== "..") {
2887
+ return base;
2888
+ }
2889
+ }
2890
+ const derived = path3.posix.basename(fallbackPath);
2891
+ if (derived && derived !== "." && derived !== "..") {
2892
+ return derived;
2893
+ }
2894
+ return "attachment.bin";
2895
+ }
2896
+ function inferMimeType(filename, explicitMimeType) {
2897
+ const explicit = explicitMimeType?.trim();
2898
+ if (explicit) {
2899
+ return explicit;
2900
+ }
2901
+ const ext = path3.extname(filename).toLowerCase();
2902
+ return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
2903
+ }
2904
+ async function detectMimeType(sandbox, targetPath) {
2905
+ try {
2906
+ const result = await sandbox.runCommand({
2907
+ cmd: "file",
2908
+ args: ["--mime-type", "-b", targetPath]
2909
+ });
2910
+ if (result.exitCode !== 0) {
2911
+ return void 0;
2912
+ }
2913
+ const value = (await result.stdout()).trim();
2914
+ return value || void 0;
2915
+ } catch {
2916
+ return void 0;
2917
+ }
2918
+ }
2919
+ function createAttachFileTool(sandbox, hooks = {}) {
2920
+ return tool({
2921
+ description: "Attach a file from the sandbox to the Slack reply. Use this immediately after creating screenshots/reports when the user asks to see/share the actual file, not just its path.",
2922
+ inputSchema: Type2.Object(
2923
+ {
2924
+ path: Type2.String({
2925
+ minLength: 1,
2926
+ description: "Absolute path (for example /tmp/screenshot.png) or workspace-relative path."
2927
+ }),
2928
+ filename: Type2.Optional(
2929
+ Type2.String({
2930
+ minLength: 1,
2931
+ description: "Optional filename override shown in Slack."
2932
+ })
2933
+ ),
2934
+ mimeType: Type2.Optional(
2935
+ Type2.String({
2936
+ minLength: 1,
2937
+ description: "Optional MIME type override (for example image/png)."
2938
+ })
2939
+ )
2940
+ },
2941
+ { additionalProperties: false }
2942
+ ),
2943
+ execute: async ({ path: requestedPath, filename, mimeType }) => {
2944
+ const targetPath = normalizeSandboxPath(requestedPath);
2945
+ const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
2946
+ if (!fileBuffer) {
2947
+ throw new Error(`failed to read file: ${targetPath}`);
2948
+ }
2949
+ if (fileBuffer.byteLength === 0) {
2950
+ throw new Error(`file is empty: ${targetPath}`);
2951
+ }
2952
+ if (fileBuffer.byteLength > MAX_ATTACH_FILE_BYTES) {
2953
+ throw new Error(
2954
+ `file exceeds ${MAX_ATTACH_FILE_BYTES} bytes: ${targetPath} (${fileBuffer.byteLength} bytes)`
2955
+ );
2956
+ }
2957
+ const resolvedFilename = sanitizeFilename(filename, targetPath);
2958
+ const detectedMimeType = await detectMimeType(sandbox, targetPath);
2959
+ const resolvedMimeType = inferMimeType(
2960
+ resolvedFilename,
2961
+ mimeType ?? detectedMimeType
2962
+ );
2963
+ const upload = {
2964
+ data: fileBuffer,
2965
+ filename: resolvedFilename,
2966
+ mimeType: resolvedMimeType
2967
+ };
2968
+ hooks.onGeneratedFiles?.([upload]);
2969
+ return {
2970
+ ok: true,
2971
+ attached: true,
2972
+ path: targetPath,
2973
+ filename: resolvedFilename,
2974
+ mime_type: resolvedMimeType,
2975
+ bytes: fileBuffer.byteLength
2976
+ };
2977
+ }
2978
+ });
2979
+ }
2980
+
2981
+ // src/chat/tools/image-generate.ts
2982
+ import { Type as Type3 } from "@sinclair/typebox";
2741
2983
 
2742
2984
  // src/chat/pi/client.ts
2743
2985
  import { completeSimple, getEnvApiKey, getModels } from "@mariozechner/pi-ai";
@@ -3018,8 +3260,8 @@ function parseImageGenerationError(status, body, model) {
3018
3260
  function createImageGenerateTool(hooks) {
3019
3261
  return tool({
3020
3262
  description: "Generate images from a prompt. Use when the user wants to visually show or represent something \u2014 feelings, concepts, art, humor, or any visual idea. Also use for explicit image creation requests.",
3021
- inputSchema: Type2.Object({
3022
- prompt: Type2.String({
3263
+ inputSchema: Type3.Object({
3264
+ prompt: Type3.String({
3023
3265
  minLength: 1,
3024
3266
  maxLength: 4e3,
3025
3267
  description: "Image generation prompt."
@@ -3095,7 +3337,7 @@ function createImageGenerateTool(hooks) {
3095
3337
  }
3096
3338
 
3097
3339
  // src/chat/tools/load-skill.ts
3098
- import { Type as Type3 } from "@sinclair/typebox";
3340
+ import { Type as Type4 } from "@sinclair/typebox";
3099
3341
  function toLoadedSkill(result) {
3100
3342
  if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
3101
3343
  return null;
@@ -3144,9 +3386,9 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
3144
3386
  }
3145
3387
  function createLoadSkillTool(sandbox, availableSkills, options) {
3146
3388
  return tool({
3147
- description: "Load a skill by name so its instructions are available for this turn. Use when a request clearly matches a known skill or a slash command references one. Do not use when no skill is relevant.",
3148
- inputSchema: Type3.Object({
3149
- skill_name: Type3.String({
3389
+ description: "Load a skill by name so its instructions are available for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
3390
+ inputSchema: Type4.Object({
3391
+ skill_name: Type4.String({
3150
3392
  minLength: 1,
3151
3393
  description: "Skill name to load, without the leading slash."
3152
3394
  })
@@ -3163,12 +3405,12 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
3163
3405
  }
3164
3406
 
3165
3407
  // src/chat/tools/read-file.ts
3166
- import { Type as Type4 } from "@sinclair/typebox";
3408
+ import { Type as Type5 } from "@sinclair/typebox";
3167
3409
  function createReadFileTool() {
3168
3410
  return tool({
3169
3411
  description: "Read a file from the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Do not use for broad discovery when search tools are better.",
3170
- inputSchema: Type4.Object({
3171
- path: Type4.String({
3412
+ inputSchema: Type5.Object({
3413
+ path: Type5.String({
3172
3414
  minLength: 1,
3173
3415
  description: "Path to the file in the sandbox workspace."
3174
3416
  })
@@ -3180,7 +3422,7 @@ function createReadFileTool() {
3180
3422
  }
3181
3423
 
3182
3424
  // src/chat/tools/slack-channel-list-messages.ts
3183
- import { Type as Type5 } from "@sinclair/typebox";
3425
+ import { Type as Type6 } from "@sinclair/typebox";
3184
3426
 
3185
3427
  // src/chat/slack-actions/channel.ts
3186
3428
  async function postMessageToChannel(input) {
@@ -3353,39 +3595,39 @@ async function listThreadReplies(input) {
3353
3595
  function createSlackChannelListMessagesTool(context) {
3354
3596
  return tool({
3355
3597
  description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
3356
- inputSchema: Type5.Object({
3357
- limit: Type5.Optional(
3358
- Type5.Integer({
3598
+ inputSchema: Type6.Object({
3599
+ limit: Type6.Optional(
3600
+ Type6.Integer({
3359
3601
  minimum: 1,
3360
3602
  maximum: 1e3,
3361
3603
  description: "Maximum number of messages to return across pages."
3362
3604
  })
3363
3605
  ),
3364
- cursor: Type5.Optional(
3365
- Type5.String({
3606
+ cursor: Type6.Optional(
3607
+ Type6.String({
3366
3608
  minLength: 1,
3367
3609
  description: "Optional cursor to continue from a prior call."
3368
3610
  })
3369
3611
  ),
3370
- oldest: Type5.Optional(
3371
- Type5.String({
3612
+ oldest: Type6.Optional(
3613
+ Type6.String({
3372
3614
  minLength: 1,
3373
3615
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
3374
3616
  })
3375
3617
  ),
3376
- latest: Type5.Optional(
3377
- Type5.String({
3618
+ latest: Type6.Optional(
3619
+ Type6.String({
3378
3620
  minLength: 1,
3379
3621
  description: "Optional latest message timestamp (Slack ts) for range filtering."
3380
3622
  })
3381
3623
  ),
3382
- inclusive: Type5.Optional(
3383
- Type5.Boolean({
3624
+ inclusive: Type6.Optional(
3625
+ Type6.Boolean({
3384
3626
  description: "Whether oldest/latest bounds should be inclusive."
3385
3627
  })
3386
3628
  ),
3387
- max_pages: Type5.Optional(
3388
- Type5.Integer({
3629
+ max_pages: Type6.Optional(
3630
+ Type6.Integer({
3389
3631
  minimum: 1,
3390
3632
  maximum: 10,
3391
3633
  description: "Maximum number of API pages to traverse in a single call."
@@ -3418,7 +3660,7 @@ function createSlackChannelListMessagesTool(context) {
3418
3660
  }
3419
3661
 
3420
3662
  // src/chat/tools/slack-channel-post-message.ts
3421
- import { Type as Type6 } from "@sinclair/typebox";
3663
+ import { Type as Type7 } from "@sinclair/typebox";
3422
3664
 
3423
3665
  // src/chat/tools/idempotency.ts
3424
3666
  function stableSerialize(value) {
@@ -3440,8 +3682,8 @@ function createOperationKey(toolName, input) {
3440
3682
  function createSlackChannelPostMessageTool(context, state) {
3441
3683
  return tool({
3442
3684
  description: "Post a message in the active Slack channel context (outside the thread). Use this when the user explicitly asks to post/send/share/say something in the channel. Do not use for normal thread replies or speculative broadcasts. Do not claim a channel message was posted unless this tool succeeds in this turn.",
3443
- inputSchema: Type6.Object({
3444
- text: Type6.String({
3685
+ inputSchema: Type7.Object({
3686
+ text: Type7.String({
3445
3687
  minLength: 1,
3446
3688
  maxLength: 4e4,
3447
3689
  description: "Slack mrkdwn text to post."
@@ -3480,13 +3722,13 @@ function createSlackChannelPostMessageTool(context, state) {
3480
3722
  }
3481
3723
 
3482
3724
  // src/chat/tools/slack-message-add-reaction.ts
3483
- import { Type as Type7 } from "@sinclair/typebox";
3725
+ import { Type as Type8 } from "@sinclair/typebox";
3484
3726
  var SLACK_EMOJI_NAME_RE = /^[a-z0-9_+-]+$/;
3485
3727
  function createSlackMessageAddReactionTool(context, state) {
3486
3728
  return tool({
3487
3729
  description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup` or `white_check_mark`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
3488
- inputSchema: Type7.Object({
3489
- emoji: Type7.String({
3730
+ inputSchema: Type8.Object({
3731
+ emoji: Type8.String({
3490
3732
  minLength: 1,
3491
3733
  maxLength: 64,
3492
3734
  description: "Slack emoji alias name to react with (for example `thumbsup` or `white_check_mark`). Optional surrounding colons are allowed."
@@ -3541,7 +3783,7 @@ function createSlackMessageAddReactionTool(context, state) {
3541
3783
  }
3542
3784
 
3543
3785
  // src/chat/tools/slack-canvas-create.ts
3544
- import { Type as Type8 } from "@sinclair/typebox";
3786
+ import { Type as Type9 } from "@sinclair/typebox";
3545
3787
 
3546
3788
  // src/chat/slack-actions/canvases.ts
3547
3789
  function normalizeCanvasMarkdown(markdown) {
@@ -3669,13 +3911,13 @@ function mergeRecentCanvases(existing, created) {
3669
3911
  function createSlackCanvasCreateTool(context, state) {
3670
3912
  return tool({
3671
3913
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when content is too long for a thread reply or needs a persistent document. Do not use for short answers that fit in-thread.",
3672
- inputSchema: Type8.Object({
3673
- title: Type8.String({
3914
+ inputSchema: Type9.Object({
3915
+ title: Type9.String({
3674
3916
  minLength: 1,
3675
3917
  maxLength: 160,
3676
3918
  description: "Canvas title."
3677
3919
  }),
3678
- markdown: Type8.String({
3920
+ markdown: Type9.String({
3679
3921
  minLength: 1,
3680
3922
  description: "Canvas markdown body content."
3681
3923
  })
@@ -3737,29 +3979,29 @@ function createSlackCanvasCreateTool(context, state) {
3737
3979
  }
3738
3980
 
3739
3981
  // src/chat/tools/slack-canvas-update.ts
3740
- import { Type as Type9 } from "@sinclair/typebox";
3982
+ import { Type as Type10 } from "@sinclair/typebox";
3741
3983
  function createSlackCanvasUpdateTool(state, _context) {
3742
3984
  return tool({
3743
3985
  description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
3744
- inputSchema: Type9.Object({
3745
- markdown: Type9.String({
3986
+ inputSchema: Type10.Object({
3987
+ markdown: Type10.String({
3746
3988
  minLength: 1,
3747
3989
  description: "Markdown content to insert or use as replacement text."
3748
3990
  }),
3749
- operation: Type9.Optional(
3750
- Type9.Union(
3751
- [Type9.Literal("insert_at_end"), Type9.Literal("insert_at_start"), Type9.Literal("replace")],
3991
+ operation: Type10.Optional(
3992
+ Type10.Union(
3993
+ [Type10.Literal("insert_at_end"), Type10.Literal("insert_at_start"), Type10.Literal("replace")],
3752
3994
  { description: "Canvas update mode." }
3753
3995
  )
3754
3996
  ),
3755
- section_id: Type9.Optional(
3756
- Type9.String({
3997
+ section_id: Type10.Optional(
3998
+ Type10.String({
3757
3999
  minLength: 1,
3758
4000
  description: "Optional section ID required for targeted replace operations."
3759
4001
  })
3760
4002
  ),
3761
- section_contains_text: Type9.Optional(
3762
- Type9.String({
4003
+ section_contains_text: Type10.Optional(
4004
+ Type10.String({
3763
4005
  minLength: 1,
3764
4006
  description: "Optional helper text used to find the target section when section_id is not provided."
3765
4007
  })
@@ -3819,7 +4061,7 @@ function createSlackCanvasUpdateTool(state, _context) {
3819
4061
  }
3820
4062
 
3821
4063
  // src/chat/tools/slack-list-add-items.ts
3822
- import { Type as Type10 } from "@sinclair/typebox";
4064
+ import { Type as Type11 } from "@sinclair/typebox";
3823
4065
 
3824
4066
  // src/chat/slack-actions/lists.ts
3825
4067
  function normalizeKey(value) {
@@ -3984,20 +4226,20 @@ async function updateListItem(input) {
3984
4226
  function createSlackListAddItemsTool(state) {
3985
4227
  return tool({
3986
4228
  description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
3987
- inputSchema: Type10.Object({
3988
- items: Type10.Array(Type10.String({ minLength: 1 }), {
4229
+ inputSchema: Type11.Object({
4230
+ items: Type11.Array(Type11.String({ minLength: 1 }), {
3989
4231
  minItems: 1,
3990
4232
  maxItems: 25,
3991
4233
  description: "List item titles to create."
3992
4234
  }),
3993
- assignee_user_id: Type10.Optional(
3994
- Type10.String({
4235
+ assignee_user_id: Type11.Optional(
4236
+ Type11.String({
3995
4237
  minLength: 1,
3996
4238
  description: "Optional Slack user ID assigned to all created items."
3997
4239
  })
3998
4240
  ),
3999
- due_date: Type10.Optional(
4000
- Type10.String({
4241
+ due_date: Type11.Optional(
4242
+ Type11.String({
4001
4243
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
4002
4244
  description: "Optional due date in YYYY-MM-DD format."
4003
4245
  })
@@ -4045,12 +4287,12 @@ function createSlackListAddItemsTool(state) {
4045
4287
  }
4046
4288
 
4047
4289
  // src/chat/tools/slack-list-create.ts
4048
- import { Type as Type11 } from "@sinclair/typebox";
4290
+ import { Type as Type12 } from "@sinclair/typebox";
4049
4291
  function createSlackListCreateTool(state) {
4050
4292
  return tool({
4051
4293
  description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
4052
- inputSchema: Type11.Object({
4053
- name: Type11.String({
4294
+ inputSchema: Type12.Object({
4295
+ name: Type12.String({
4054
4296
  minLength: 1,
4055
4297
  maxLength: 160,
4056
4298
  description: "Name for the new Slack list."
@@ -4084,13 +4326,13 @@ function createSlackListCreateTool(state) {
4084
4326
  }
4085
4327
 
4086
4328
  // src/chat/tools/slack-list-get-items.ts
4087
- import { Type as Type12 } from "@sinclair/typebox";
4329
+ import { Type as Type13 } from "@sinclair/typebox";
4088
4330
  function createSlackListGetItemsTool(state) {
4089
4331
  return tool({
4090
4332
  description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
4091
- inputSchema: Type12.Object({
4092
- limit: Type12.Optional(
4093
- Type12.Integer({
4333
+ inputSchema: Type13.Object({
4334
+ limit: Type13.Optional(
4335
+ Type13.Integer({
4094
4336
  minimum: 1,
4095
4337
  maximum: 200,
4096
4338
  description: "Maximum number of list items to return."
@@ -4114,23 +4356,23 @@ function createSlackListGetItemsTool(state) {
4114
4356
  }
4115
4357
 
4116
4358
  // src/chat/tools/slack-list-update-item.ts
4117
- import { Type as Type13 } from "@sinclair/typebox";
4359
+ import { Type as Type14 } from "@sinclair/typebox";
4118
4360
  function createSlackListUpdateItemTool(state) {
4119
4361
  return tool({
4120
4362
  description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
4121
- inputSchema: Type13.Object(
4363
+ inputSchema: Type14.Object(
4122
4364
  {
4123
- item_id: Type13.String({
4365
+ item_id: Type14.String({
4124
4366
  minLength: 1,
4125
4367
  description: "ID of the Slack list item to update."
4126
4368
  }),
4127
- completed: Type13.Optional(
4128
- Type13.Boolean({
4369
+ completed: Type14.Optional(
4370
+ Type14.Boolean({
4129
4371
  description: "Optional completion status update."
4130
4372
  })
4131
4373
  ),
4132
- title: Type13.Optional(
4133
- Type13.String({
4374
+ title: Type14.Optional(
4375
+ Type14.String({
4134
4376
  minLength: 1,
4135
4377
  description: "Optional new item title."
4136
4378
  })
@@ -4180,11 +4422,11 @@ function createSlackListUpdateItemTool(state) {
4180
4422
  }
4181
4423
 
4182
4424
  // src/chat/tools/system-time.ts
4183
- import { Type as Type14 } from "@sinclair/typebox";
4425
+ import { Type as Type15 } from "@sinclair/typebox";
4184
4426
  function createSystemTimeTool() {
4185
4427
  return tool({
4186
4428
  description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
4187
- inputSchema: Type14.Object({}),
4429
+ inputSchema: Type15.Object({}),
4188
4430
  execute: async () => {
4189
4431
  const now = /* @__PURE__ */ new Date();
4190
4432
  return {
@@ -4199,7 +4441,7 @@ function createSystemTimeTool() {
4199
4441
  }
4200
4442
 
4201
4443
  // src/chat/tools/web-fetch.ts
4202
- import { Type as Type15 } from "@sinclair/typebox";
4444
+ import { Type as Type16 } from "@sinclair/typebox";
4203
4445
 
4204
4446
  // src/chat/tools/constants.ts
4205
4447
  var USER_AGENT = "junior-bot/0.1";
@@ -4528,13 +4770,13 @@ function extractHttpStatusFromMessage(message) {
4528
4770
  function createWebFetchTool(hooks) {
4529
4771
  return tool({
4530
4772
  description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
4531
- inputSchema: Type15.Object({
4532
- url: Type15.String({
4773
+ inputSchema: Type16.Object({
4774
+ url: Type16.String({
4533
4775
  minLength: 1,
4534
4776
  description: "HTTP(S) URL to fetch."
4535
4777
  }),
4536
- max_chars: Type15.Optional(
4537
- Type15.Integer({
4778
+ max_chars: Type16.Optional(
4779
+ Type16.Integer({
4538
4780
  minimum: 500,
4539
4781
  maximum: MAX_FETCH_CHARS,
4540
4782
  description: "Optional maximum number of extracted characters to return."
@@ -4587,7 +4829,7 @@ function createWebFetchTool(hooks) {
4587
4829
  // src/chat/tools/web-search.ts
4588
4830
  import { generateText } from "ai";
4589
4831
  import { createGatewayProvider } from "@ai-sdk/gateway";
4590
- import { Type as Type16 } from "@sinclair/typebox";
4832
+ import { Type as Type17 } from "@sinclair/typebox";
4591
4833
  var SEARCH_TIMEOUT_MS = 1e4;
4592
4834
  var MAX_RESULTS = 5;
4593
4835
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
@@ -4638,14 +4880,14 @@ function isTimeoutSearchFailure(message) {
4638
4880
  function createWebSearchTool() {
4639
4881
  return tool({
4640
4882
  description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
4641
- inputSchema: Type16.Object({
4642
- query: Type16.String({
4883
+ inputSchema: Type17.Object({
4884
+ query: Type17.String({
4643
4885
  minLength: 1,
4644
4886
  maxLength: 500,
4645
4887
  description: "Search query."
4646
4888
  }),
4647
- max_results: Type16.Optional(
4648
- Type16.Integer({
4889
+ max_results: Type17.Optional(
4890
+ Type17.Integer({
4649
4891
  minimum: 1,
4650
4892
  maximum: MAX_RESULTS,
4651
4893
  description: "Max results to return."
@@ -4710,16 +4952,16 @@ function createWebSearchTool() {
4710
4952
  }
4711
4953
 
4712
4954
  // src/chat/tools/write-file.ts
4713
- import { Type as Type17 } from "@sinclair/typebox";
4955
+ import { Type as Type18 } from "@sinclair/typebox";
4714
4956
  function createWriteFileTool() {
4715
4957
  return tool({
4716
4958
  description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
4717
- inputSchema: Type17.Object({
4718
- path: Type17.String({
4959
+ inputSchema: Type18.Object({
4960
+ path: Type18.String({
4719
4961
  minLength: 1,
4720
4962
  description: "Path to write in the sandbox workspace."
4721
4963
  }),
4722
- content: Type17.String({
4964
+ content: Type18.String({
4723
4965
  description: "UTF-8 file content to write."
4724
4966
  })
4725
4967
  }, { additionalProperties: false }),
@@ -4793,15 +5035,40 @@ function createTools(availableSkills, hooks = {}, context) {
4793
5035
  ),
4794
5036
  systemTime: wrapToolExecution("systemTime", createSystemTimeTool(), hooks),
4795
5037
  bash: wrapToolExecution("bash", createBashTool(), hooks),
5038
+ attachFile: wrapToolExecution(
5039
+ "attachFile",
5040
+ createAttachFileTool(context.sandbox, hooks),
5041
+ hooks
5042
+ ),
4796
5043
  readFile: wrapToolExecution("readFile", createReadFileTool(), hooks),
4797
5044
  writeFile: wrapToolExecution("writeFile", createWriteFileTool(), hooks),
4798
5045
  webSearch: wrapToolExecution("webSearch", createWebSearchTool(), hooks),
4799
5046
  webFetch: wrapToolExecution("webFetch", createWebFetchTool(hooks), hooks),
4800
- imageGenerate: wrapToolExecution("imageGenerate", createImageGenerateTool(hooks), hooks),
4801
- slackCanvasUpdate: wrapToolExecution("slackCanvasUpdate", createSlackCanvasUpdateTool(state, context), hooks),
4802
- slackListCreate: wrapToolExecution("slackListCreate", createSlackListCreateTool(state), hooks),
4803
- slackListAddItems: wrapToolExecution("slackListAddItems", createSlackListAddItemsTool(state), hooks),
4804
- slackListGetItems: wrapToolExecution("slackListGetItems", createSlackListGetItemsTool(state), hooks),
5047
+ imageGenerate: wrapToolExecution(
5048
+ "imageGenerate",
5049
+ createImageGenerateTool(hooks),
5050
+ hooks
5051
+ ),
5052
+ slackCanvasUpdate: wrapToolExecution(
5053
+ "slackCanvasUpdate",
5054
+ createSlackCanvasUpdateTool(state, context),
5055
+ hooks
5056
+ ),
5057
+ slackListCreate: wrapToolExecution(
5058
+ "slackListCreate",
5059
+ createSlackListCreateTool(state),
5060
+ hooks
5061
+ ),
5062
+ slackListAddItems: wrapToolExecution(
5063
+ "slackListAddItems",
5064
+ createSlackListAddItemsTool(state),
5065
+ hooks
5066
+ ),
5067
+ slackListGetItems: wrapToolExecution(
5068
+ "slackListGetItems",
5069
+ createSlackListGetItemsTool(state),
5070
+ hooks
5071
+ ),
4805
5072
  slackListUpdateItem: wrapToolExecution(
4806
5073
  "slackListUpdateItem",
4807
5074
  createSlackListUpdateItemTool(state),
@@ -4839,7 +5106,7 @@ function createTools(availableSkills, hooks = {}, context) {
4839
5106
 
4840
5107
  // src/chat/sandbox/sandbox.ts
4841
5108
  import fs4 from "fs/promises";
4842
- import path3 from "path";
5109
+ import path4 from "path";
4843
5110
  import { Sandbox } from "@vercel/sandbox";
4844
5111
  import { createBashTool as createBashTool2 } from "bash-tool";
4845
5112
 
@@ -4939,20 +5206,32 @@ function extractHttpErrorDetails(error, options = {}) {
4939
5206
  // src/chat/sandbox/sandbox.ts
4940
5207
  var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
4941
5208
  var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
5209
+ var SANDBOX_RUNTIME = "node22";
4942
5210
  var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
4943
- var SANDBOX_ERROR_FIELDS = [{ sourceKey: "sandboxId", attributeKey: "sandbox_id", summaryKey: "sandboxId" }];
5211
+ var SANDBOX_ERROR_FIELDS = [
5212
+ {
5213
+ sourceKey: "sandboxId",
5214
+ attributeKey: "sandbox_id",
5215
+ summaryKey: "sandboxId"
5216
+ }
5217
+ ];
4944
5218
  function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
4945
5219
  const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
4946
5220
  const existingAllowRaw = basePolicy.allow;
4947
5221
  const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
4948
- Object.entries(existingAllowRaw).map(([domain, rules]) => [
4949
- domain,
4950
- Array.isArray(rules) ? [...rules] : []
4951
- ])
5222
+ Object.entries(existingAllowRaw).map(
5223
+ ([domain, rules]) => [
5224
+ domain,
5225
+ Array.isArray(rules) ? [...rules] : []
5226
+ ]
5227
+ )
4952
5228
  ) : { "*": [] };
4953
5229
  for (const transform of headerTransforms) {
4954
5230
  const currentRules = existingAllow[transform.domain] ?? [];
4955
- existingAllow[transform.domain] = [...currentRules, { transform: [{ headers: transform.headers }] }];
5231
+ existingAllow[transform.domain] = [
5232
+ ...currentRules,
5233
+ { transform: [{ headers: transform.headers }] }
5234
+ ];
4956
5235
  }
4957
5236
  return {
4958
5237
  ...basePolicy,
@@ -4972,7 +5251,7 @@ function truncateOutput(output, maxLength) {
4972
5251
  };
4973
5252
  }
4974
5253
  function toPosixRelative(base, absolute) {
4975
- return path3.relative(base, absolute).split(path3.sep).join("/");
5254
+ return path4.relative(base, absolute).split(path4.sep).join("/");
4976
5255
  }
4977
5256
  async function listFilesRecursive(root) {
4978
5257
  const queue = [root];
@@ -4982,7 +5261,7 @@ async function listFilesRecursive(root) {
4982
5261
  const entries = await fs4.readdir(dir, { withFileTypes: true });
4983
5262
  entries.sort((a, b) => a.name.localeCompare(b.name));
4984
5263
  for (const entry of entries) {
4985
- const absolute = path3.join(dir, entry.name);
5264
+ const absolute = path4.join(dir, entry.name);
4986
5265
  if (entry.isDirectory()) {
4987
5266
  queue.push(absolute);
4988
5267
  } else if (entry.isFile()) {
@@ -5024,7 +5303,7 @@ async function buildSkillSyncFiles(availableSkills) {
5024
5303
  function collectDirectories(filesToWrite) {
5025
5304
  const directoriesToEnsure = /* @__PURE__ */ new Set();
5026
5305
  for (const file of filesToWrite) {
5027
- const normalizedPath = path3.posix.normalize(file.path);
5306
+ const normalizedPath = path4.posix.normalize(file.path);
5028
5307
  const parts = normalizedPath.split("/").filter(Boolean);
5029
5308
  let current = "";
5030
5309
  for (let index = 0; index < parts.length - 1; index += 1) {
@@ -5032,7 +5311,9 @@ function collectDirectories(filesToWrite) {
5032
5311
  directoriesToEnsure.add(current);
5033
5312
  }
5034
5313
  }
5035
- return Array.from(directoriesToEnsure).filter((directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)).sort((a, b) => a.length - b.length);
5314
+ return Array.from(directoriesToEnsure).filter(
5315
+ (directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
5316
+ ).sort((a, b) => a.length - b.length);
5036
5317
  }
5037
5318
  function getSandboxErrorDetails(error) {
5038
5319
  return extractHttpErrorDetails(error, {
@@ -5086,7 +5367,9 @@ function wrapSandboxSetupError(error) {
5086
5367
  try {
5087
5368
  const details = getSandboxErrorDetails(error);
5088
5369
  if (details.summary) {
5089
- return new Error(`sandbox setup failed (${details.summary})`, { cause: error });
5370
+ return new Error(`sandbox setup failed (${details.summary})`, {
5371
+ cause: error
5372
+ });
5090
5373
  }
5091
5374
  } catch {
5092
5375
  }
@@ -5112,9 +5395,12 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
5112
5395
  "app.sandbox.success": false
5113
5396
  });
5114
5397
  setSpanStatus("error");
5115
- throw new Error(details.summary ? `${action} failed (${details.summary})` : `${action} failed`, {
5116
- cause: error
5117
- });
5398
+ throw new Error(
5399
+ details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
5400
+ {
5401
+ cause: error
5402
+ }
5403
+ );
5118
5404
  }
5119
5405
  function createSandboxExecutor(options) {
5120
5406
  let sandbox = null;
@@ -5124,6 +5410,7 @@ function createSandboxExecutor(options) {
5124
5410
  const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
5125
5411
  const traceContext = options?.traceContext ?? {};
5126
5412
  const emitStatus = options?.onStatus;
5413
+ const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
5127
5414
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
5128
5415
  const invalidateSandboxInstance = async (targetSandbox, reason) => {
5129
5416
  if (sandbox === targetSandbox) {
@@ -5153,7 +5440,10 @@ function createSandboxExecutor(options) {
5153
5440
  },
5154
5441
  async () => {
5155
5442
  const filesToWrite = await buildSkillSyncFiles(availableSkills);
5156
- const bytesWritten = filesToWrite.reduce((total, file) => total + file.content.length, 0);
5443
+ const bytesWritten = filesToWrite.reduce(
5444
+ (total, file) => total + file.content.length,
5445
+ 0
5446
+ );
5157
5447
  const directories = collectDirectories(filesToWrite);
5158
5448
  await withSandboxSpan(
5159
5449
  "sandbox.sync_writeFiles",
@@ -5204,7 +5494,7 @@ function createSandboxExecutor(options) {
5204
5494
  throw wrapSandboxSetupError(error);
5205
5495
  };
5206
5496
  const createFreshSandbox = async () => {
5207
- const runtime = "node22";
5497
+ const runtime = SANDBOX_RUNTIME;
5208
5498
  let statusCount = 0;
5209
5499
  const sentStatuses = /* @__PURE__ */ new Set();
5210
5500
  const emitSandboxStatus = async (status) => {
@@ -5253,9 +5543,13 @@ function createSandboxExecutor(options) {
5253
5543
  "app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
5254
5544
  "app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
5255
5545
  "app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
5256
- ...snapshot.profileHash ? { "app.sandbox.snapshot.profile_hash": snapshot.profileHash } : {},
5546
+ ...snapshot.profileHash ? {
5547
+ "app.sandbox.snapshot.profile_hash": snapshot.profileHash
5548
+ } : {},
5257
5549
  "app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
5258
- ...snapshot.rebuildReason ? { "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason } : {}
5550
+ ...snapshot.rebuildReason ? {
5551
+ "app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
5552
+ } : {}
5259
5553
  });
5260
5554
  if (!snapshot.snapshotId) {
5261
5555
  await emitSandboxStatus("Starting sandbox...");
@@ -5290,7 +5584,9 @@ function createSandboxExecutor(options) {
5290
5584
  if (!rebuiltSnapshot.snapshotId) {
5291
5585
  throw error;
5292
5586
  }
5293
- await emitSandboxStatus("Retrying sandbox startup with a fresh snapshot...");
5587
+ await emitSandboxStatus(
5588
+ "Retrying sandbox startup with a fresh snapshot..."
5589
+ );
5294
5590
  return await Sandbox.create({
5295
5591
  timeout: timeoutMs,
5296
5592
  source: {
@@ -5311,6 +5607,17 @@ function createSandboxExecutor(options) {
5311
5607
  }
5312
5608
  return assignSandbox(createdSandbox);
5313
5609
  };
5610
+ if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
5611
+ setSpanAttributes({
5612
+ "app.sandbox.reused": false,
5613
+ "app.sandbox.recreate.reason": "dependency_profile_mismatch",
5614
+ ...options?.sandboxDependencyProfileHash ? {
5615
+ "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
5616
+ } : {},
5617
+ ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
5618
+ });
5619
+ sandboxIdHint = void 0;
5620
+ }
5314
5621
  const recoverUnavailableSandbox = async (source) => {
5315
5622
  setSpanAttributes({
5316
5623
  "app.sandbox.recovery.attempted": true,
@@ -5405,11 +5712,16 @@ function createSandboxExecutor(options) {
5405
5712
  const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
5406
5713
  const headerTransforms = input.headerTransforms;
5407
5714
  if (headerTransforms && headerTransforms.length > 0) {
5408
- const policy = mergeNetworkPolicyWithHeaderTransforms(restoreNetworkPolicy, headerTransforms);
5715
+ const policy = mergeNetworkPolicyWithHeaderTransforms(
5716
+ restoreNetworkPolicy,
5717
+ headerTransforms
5718
+ );
5409
5719
  await activeSandbox.updateNetworkPolicy(policy);
5410
5720
  }
5411
5721
  const pathPrefix = `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`;
5412
- const envExports = input.env ? Object.entries(input.env).map(([key, value]) => `export ${key}='${value.replace(/'/g, "'\\''")}'`).join(" && ") : "";
5722
+ const envExports = input.env ? Object.entries(input.env).map(
5723
+ ([key, value]) => `export ${key}='${value.replace(/'/g, "'\\''")}'`
5724
+ ).join(" && ") : "";
5413
5725
  const preamble = envExports ? `export PATH="${pathPrefix}" && ${envExports}` : `export PATH="${pathPrefix}"`;
5414
5726
  let commandError;
5415
5727
  try {
@@ -5418,7 +5730,10 @@ function createSandboxExecutor(options) {
5418
5730
  args: ["-c", `${preamble} && ${input.command}`],
5419
5731
  cwd: SANDBOX_WORKSPACE_ROOT
5420
5732
  });
5421
- const maxOutputLength = Number.parseInt(process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "", 10);
5733
+ const maxOutputLength = Number.parseInt(
5734
+ process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
5735
+ 10
5736
+ );
5422
5737
  const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
5423
5738
  const stdoutRaw = await commandResult2.stdout();
5424
5739
  const stderrRaw = await commandResult2.stderr();
@@ -5473,7 +5788,10 @@ function createSandboxExecutor(options) {
5473
5788
  }
5474
5789
  }
5475
5790
  const activeSandbox = await createSandbox();
5476
- const keepAliveMs = Number.parseInt(process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0", 10);
5791
+ const keepAliveMs = Number.parseInt(
5792
+ process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
5793
+ 10
5794
+ );
5477
5795
  if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
5478
5796
  try {
5479
5797
  await withSandboxSpan(
@@ -5492,12 +5810,18 @@ function createSandboxExecutor(options) {
5492
5810
  if (params.toolName === "bash") {
5493
5811
  const command = bashCommand;
5494
5812
  const headerTransformsInput = rawInput.headerTransforms;
5495
- const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter((value) => Boolean(value && typeof value === "object")).map((transform) => ({
5813
+ const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
5814
+ (value) => Boolean(value && typeof value === "object")
5815
+ ).map((transform) => ({
5496
5816
  domain: String(transform.domain ?? "").trim(),
5497
5817
  headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
5498
- Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
5818
+ Object.entries(
5819
+ transform.headers
5820
+ ).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
5499
5821
  ) : {}
5500
- })).filter((transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0) : void 0;
5822
+ })).filter(
5823
+ (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
5824
+ ) : void 0;
5501
5825
  const envInput = rawInput.env;
5502
5826
  const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
5503
5827
  Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
@@ -5511,11 +5835,21 @@ function createSandboxExecutor(options) {
5511
5835
  },
5512
5836
  async () => {
5513
5837
  try {
5514
- const response = await executeBash({ command, ...headerTransforms ? { headerTransforms } : {}, ...env ? { env } : {} });
5838
+ const response = await executeBash({
5839
+ command,
5840
+ ...headerTransforms ? { headerTransforms } : {},
5841
+ ...env ? { env } : {}
5842
+ });
5515
5843
  setSpanAttributes({
5516
5844
  "process.exit.code": response.exitCode,
5517
- "app.sandbox.stdout_bytes": Buffer.byteLength(response.stdout ?? "", "utf8"),
5518
- "app.sandbox.stderr_bytes": Buffer.byteLength(response.stderr ?? "", "utf8"),
5845
+ "app.sandbox.stdout_bytes": Buffer.byteLength(
5846
+ response.stdout ?? "",
5847
+ "utf8"
5848
+ ),
5849
+ "app.sandbox.stderr_bytes": Buffer.byteLength(
5850
+ response.stderr ?? "",
5851
+ "utf8"
5852
+ ),
5519
5853
  ...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
5520
5854
  });
5521
5855
  setSpanStatus(response.exitCode === 0 ? "ok" : "error");
@@ -5630,6 +5964,9 @@ function createSandboxExecutor(options) {
5630
5964
  getSandboxId() {
5631
5965
  return sandbox?.sandboxId ?? sandboxIdHint;
5632
5966
  },
5967
+ getDependencyProfileHash() {
5968
+ return dependencyProfileHash;
5969
+ },
5633
5970
  canExecute(toolName) {
5634
5971
  return SANDBOX_TOOL_NAMES.has(toolName);
5635
5972
  },
@@ -5742,6 +6079,35 @@ function isRetryableTurnError(error, reason) {
5742
6079
  return error.reason === reason;
5743
6080
  }
5744
6081
 
6082
+ // src/chat/attachment-claims.ts
6083
+ function splitSentences(text) {
6084
+ return text.split(/\n+/).flatMap((line) => line.split(/(?<=[.!?])\s+/)).map((part) => part.trim()).filter((part) => part.length > 0);
6085
+ }
6086
+ function sentenceClaimsAttachment(sentence) {
6087
+ const hasAttachmentNoun = /\b(screenshot|image|file|attachment)\b/i.test(
6088
+ sentence
6089
+ );
6090
+ if (!hasAttachmentNoun) {
6091
+ return false;
6092
+ }
6093
+ const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
6094
+ const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
6095
+ return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
6096
+ }
6097
+ function claimsAttachment(text) {
6098
+ return splitSentences(text).some(
6099
+ (sentence) => sentenceClaimsAttachment(sentence)
6100
+ );
6101
+ }
6102
+ function enforceAttachmentClaimTruth(text, hasAttachedFiles) {
6103
+ if (hasAttachedFiles || !claimsAttachment(text)) {
6104
+ return text;
6105
+ }
6106
+ return `${text}
6107
+
6108
+ Note: No file was attached in this turn. I need to attach the file before claiming it is shared.`;
6109
+ }
6110
+
5745
6111
  // src/chat/respond.ts
5746
6112
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
5747
6113
  var startupDiscoveryLogged = false;
@@ -5809,7 +6175,8 @@ function isToolPayloadShape(payload) {
5809
6175
  const record = payload;
5810
6176
  const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
5811
6177
  if (type.startsWith("tool-")) return true;
5812
- if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error") return true;
6178
+ if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
6179
+ return true;
5813
6180
  const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
5814
6181
  const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
5815
6182
  if (hasToolName && hasToolInput) return true;
@@ -5853,7 +6220,7 @@ function formatToolStatus(toolName) {
5853
6220
  }
5854
6221
  function formatToolStatusWithInput(toolName, input) {
5855
6222
  const obj = input && typeof input === "object" ? input : void 0;
5856
- const path5 = obj ? compactStatusPath(obj.path) : void 0;
6223
+ const path6 = obj ? compactStatusPath(obj.path) : void 0;
5857
6224
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
5858
6225
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
5859
6226
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
@@ -5861,8 +6228,8 @@ function formatToolStatusWithInput(toolName, input) {
5861
6228
  if (filename && toolName === "readFile") {
5862
6229
  return `Reading file ${filename}`;
5863
6230
  }
5864
- if (path5 && toolName === "writeFile") {
5865
- return `Writing file ${path5}`;
6231
+ if (path6 && toolName === "writeFile") {
6232
+ return `Writing file ${path6}`;
5866
6233
  }
5867
6234
  if (skillName && toolName === "loadSkill") {
5868
6235
  return `Loading skill ${skillName}`;
@@ -5902,7 +6269,7 @@ function formatToolResultStatus(toolName) {
5902
6269
  }
5903
6270
  function formatToolResultStatusWithInput(toolName, input) {
5904
6271
  const obj = input && typeof input === "object" ? input : void 0;
5905
- const path5 = obj ? compactStatusPath(obj.path) : void 0;
6272
+ const path6 = obj ? compactStatusPath(obj.path) : void 0;
5906
6273
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
5907
6274
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
5908
6275
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
@@ -5910,8 +6277,8 @@ function formatToolResultStatusWithInput(toolName, input) {
5910
6277
  if (filename && toolName === "readFile") {
5911
6278
  return `Reviewed file ${filename}`;
5912
6279
  }
5913
- if (path5 && toolName === "writeFile") {
5914
- return `Saved file ${path5}`;
6280
+ if (path6 && toolName === "writeFile") {
6281
+ return `Saved file ${path6}`;
5915
6282
  }
5916
6283
  if (skillName && toolName === "loadSkill") {
5917
6284
  return `Loaded skill ${skillName}`;
@@ -6013,11 +6380,16 @@ function isAssistantMessage(value) {
6013
6380
  }
6014
6381
  function extractAssistantText(message) {
6015
6382
  const content = message.content ?? [];
6016
- return content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
6383
+ return content.filter(
6384
+ (part) => part.type === "text" && typeof part.text === "string"
6385
+ ).map((part) => part.text).join("\n");
6017
6386
  }
6018
6387
  function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
6019
6388
  const keys = /* @__PURE__ */ new Set();
6020
- for (const skill of [...activeSkills, ...explicitSkill ? [explicitSkill] : []]) {
6389
+ for (const skill of [
6390
+ ...activeSkills,
6391
+ ...explicitSkill ? [explicitSkill] : []
6392
+ ]) {
6021
6393
  for (const key of skill.usesConfig ?? []) {
6022
6394
  keys.add(key);
6023
6395
  }
@@ -6122,7 +6494,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
6122
6494
  ...toolResultAttribute2 ? { "gen_ai.tool.call.result": toolResultAttribute2 } : {}
6123
6495
  });
6124
6496
  setSpanStatus("ok");
6125
- await onStatus?.(`${formatToolResultStatusWithInput(toolName, parsed)}...`);
6497
+ await onStatus?.(
6498
+ `${formatToolResultStatusWithInput(toolName, parsed)}...`
6499
+ );
6126
6500
  if (shouldTrace) {
6127
6501
  logInfo(
6128
6502
  "agent_tool_call_completed",
@@ -6150,7 +6524,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
6150
6524
  const isCustomBashCommand = toolName === "bash" && /^jr-rpc(?:\s|$)/.test(bashCommand);
6151
6525
  const shouldLogCredentialInjection = toolName === "bash" && !isCustomBashCommand && Boolean(injectedHeaders && injectedHeaders.length > 0);
6152
6526
  if (shouldLogCredentialInjection) {
6153
- const headerDomains = (injectedHeaders ?? []).map((transform) => transform.domain);
6527
+ const headerDomains = (injectedHeaders ?? []).map(
6528
+ (transform) => transform.domain
6529
+ );
6154
6530
  logInfo(
6155
6531
  "credential_inject_start",
6156
6532
  {},
@@ -6163,7 +6539,10 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
6163
6539
  );
6164
6540
  }
6165
6541
  const hasBashCredentials = injectedHeaders || injectedEnv;
6166
- const sandboxInput = toolName === "bash" ? { command: String(parsed.command ?? "") } : toolName === "readFile" ? { path: String(parsed.path ?? "") } : toolName === "writeFile" ? { path: String(parsed.path ?? ""), content: String(parsed.content ?? "") } : parsed;
6542
+ const sandboxInput = toolName === "bash" ? { command: String(parsed.command ?? "") } : toolName === "readFile" ? { path: String(parsed.path ?? "") } : toolName === "writeFile" ? {
6543
+ path: String(parsed.path ?? ""),
6544
+ content: String(parsed.content ?? "")
6545
+ } : parsed;
6167
6546
  const result = sandboxExecutor?.canExecute(toolName) ? await sandboxExecutor.execute({
6168
6547
  toolName,
6169
6548
  input: toolName === "bash" && hasBashCredentials ? {
@@ -6193,7 +6572,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
6193
6572
  ...toolResultAttribute ? { "gen_ai.tool.call.result": toolResultAttribute } : {}
6194
6573
  });
6195
6574
  setSpanStatus("ok");
6196
- await onStatus?.(`${formatToolResultStatusWithInput(toolName, parsed)}...`);
6575
+ await onStatus?.(
6576
+ `${formatToolResultStatusWithInput(toolName, parsed)}...`
6577
+ );
6197
6578
  if (shouldTrace) {
6198
6579
  logInfo(
6199
6580
  "agent_tool_call_completed",
@@ -6211,7 +6592,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
6211
6592
  );
6212
6593
  }
6213
6594
  return {
6214
- content: [{ type: "text", text: toToolContentText(resultDetails) }],
6595
+ content: [
6596
+ { type: "text", text: toToolContentText(resultDetails) }
6597
+ ],
6215
6598
  details: resultDetails
6216
6599
  };
6217
6600
  } catch (error) {
@@ -6272,6 +6655,8 @@ async function generateAssistantReply(messageText, context = {}) {
6272
6655
  let timeoutResumeSessionId;
6273
6656
  let timeoutResumeSliceId = 1;
6274
6657
  let timeoutResumeMessages = [];
6658
+ let lastKnownSandboxId = context.sandbox?.sandboxId;
6659
+ let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
6275
6660
  try {
6276
6661
  const shouldTrace = shouldEmitDevAgentTrace();
6277
6662
  const spanContext = {
@@ -6285,11 +6670,15 @@ async function generateAssistantReply(messageText, context = {}) {
6285
6670
  assistantUserName: context.assistant?.userName,
6286
6671
  modelId: botConfig.modelId
6287
6672
  };
6288
- const availableSkills = await discoverSkills({ additionalRoots: context.skillDirs });
6673
+ const availableSkills = await discoverSkills({
6674
+ additionalRoots: context.skillDirs
6675
+ });
6289
6676
  if (!startupDiscoveryLogged) {
6290
6677
  startupDiscoveryLogged = true;
6291
6678
  const plugins = getPluginProviders();
6292
- const roots = [...new Set(availableSkills.map((skill) => skill.skillPath))].sort();
6679
+ const roots = [
6680
+ ...new Set(availableSkills.map((skill) => skill.skillPath))
6681
+ ].sort();
6293
6682
  logInfo(
6294
6683
  "startup_discovery_summary",
6295
6684
  spanContext,
@@ -6321,17 +6710,18 @@ async function generateAssistantReply(messageText, context = {}) {
6321
6710
  "Agent message received"
6322
6711
  );
6323
6712
  }
6324
- const explicitInvocation = parseSkillInvocation(userInput, availableSkills);
6325
- const explicitSkill = explicitInvocation ? findSkillByName(explicitInvocation.skillName, availableSkills) : null;
6713
+ const skillInvocation = parseSkillInvocation(userInput, availableSkills);
6714
+ const invokedSkill = skillInvocation ? findSkillByName(skillInvocation.skillName, availableSkills) : null;
6326
6715
  const activeSkills = [];
6327
6716
  const skillSandbox = new SkillSandbox(availableSkills, activeSkills);
6328
6717
  const capabilityRuntime = createSkillCapabilityRuntime({
6329
- invocationArgs: explicitInvocation?.args,
6718
+ invocationArgs: skillInvocation?.args,
6330
6719
  requesterId: context.requester?.userId,
6331
6720
  resolveConfiguration: async (key) => configurationValues[key]
6332
6721
  });
6333
6722
  const sandboxExecutor = createSandboxExecutor({
6334
6723
  sandboxId: context.sandbox?.sandboxId,
6724
+ sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
6335
6725
  traceContext: spanContext,
6336
6726
  onStatus: context.onStatus,
6337
6727
  runBashCustomCommand: async (command) => {
@@ -6355,20 +6745,26 @@ async function generateAssistantReply(messageText, context = {}) {
6355
6745
  return result.handled ? { handled: true, result: result.result } : { handled: false };
6356
6746
  }
6357
6747
  });
6748
+ lastKnownSandboxId = sandboxExecutor.getSandboxId();
6749
+ lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
6358
6750
  sandboxExecutor.configureSkills(availableSkills);
6359
6751
  const sandbox = await sandboxExecutor.createSandbox();
6360
- if (explicitSkill) {
6361
- const preloaded = await skillSandbox.loadSkill(explicitSkill.name);
6752
+ if (invokedSkill && skillInvocation?.source === "hard_bang") {
6753
+ const preloaded = await skillSandbox.loadSkill(invokedSkill.name);
6362
6754
  if (preloaded) {
6363
6755
  activeSkills.push(preloaded);
6364
6756
  }
6365
6757
  }
6366
- const userTurnText = buildUserTurnText(userInput, context.conversationContext);
6758
+ const userTurnText = buildUserTurnText(
6759
+ userInput,
6760
+ context.conversationContext
6761
+ );
6367
6762
  if (!getGatewayApiKey()) {
6368
6763
  const providerError = "Missing AI gateway credentials (AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN)";
6369
6764
  return {
6370
6765
  text: `Error: ${providerError}`,
6371
6766
  sandboxId: sandboxExecutor.getSandboxId(),
6767
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
6372
6768
  diagnostics: {
6373
6769
  outcome: "provider_error",
6374
6770
  modelId: botConfig.modelId,
@@ -6405,15 +6801,21 @@ async function generateAssistantReply(messageText, context = {}) {
6405
6801
  Object.assign(artifactStatePatch, patch);
6406
6802
  },
6407
6803
  onToolCallStart: async (toolName, input) => {
6408
- await context.onStatus?.(`${formatToolStatusWithInput(toolName, input)}...`);
6804
+ await context.onStatus?.(
6805
+ `${formatToolStatusWithInput(toolName, input)}...`
6806
+ );
6409
6807
  },
6410
6808
  onToolCallEnd: async (toolName, input) => {
6411
- await context.onStatus?.(`${formatToolResultStatusWithInput(toolName, input)}...`);
6809
+ await context.onStatus?.(
6810
+ `${formatToolResultStatusWithInput(toolName, input)}...`
6811
+ );
6412
6812
  },
6413
6813
  onSkillLoaded: async (loadedSkill) => {
6414
6814
  const resolvedSkill = await skillSandbox.loadSkill(loadedSkill.name);
6415
6815
  const effective = resolvedSkill ?? loadedSkill;
6416
- const existing = activeSkills.find((skill) => skill.name === effective.name);
6816
+ const existing = activeSkills.find(
6817
+ (skill) => skill.name === effective.name
6818
+ );
6417
6819
  if (existing) {
6418
6820
  existing.body = effective.body;
6419
6821
  existing.description = effective.description;
@@ -6439,17 +6841,18 @@ async function generateAssistantReply(messageText, context = {}) {
6439
6841
  const baseInstructions = buildSystemPrompt({
6440
6842
  availableSkills,
6441
6843
  activeSkills,
6442
- invocation: explicitInvocation,
6844
+ invocation: skillInvocation,
6443
6845
  assistant: context.assistant,
6444
6846
  requester: context.requester,
6445
6847
  artifactState: context.artifactState,
6446
6848
  configuration: configurationValues,
6447
- relevantConfigurationKeys: collectRelevantConfigurationKeys(activeSkills, explicitSkill),
6849
+ relevantConfigurationKeys: collectRelevantConfigurationKeys(
6850
+ activeSkills,
6851
+ invokedSkill
6852
+ ),
6448
6853
  runtimeMetadata: getRuntimeMetadata()
6449
6854
  });
6450
- const userContentParts = [
6451
- { type: "text", text: userTurnText }
6452
- ];
6855
+ const userContentParts = [{ type: "text", text: userTurnText }];
6453
6856
  for (const attachment of context.userAttachments ?? []) {
6454
6857
  if (attachment.mediaType.startsWith("image/")) {
6455
6858
  userContentParts.push({
@@ -6532,7 +6935,9 @@ async function generateAssistantReply(messageText, context = {}) {
6532
6935
  logWarn(
6533
6936
  "streaming_text_delta_error",
6534
6937
  {},
6535
- { "error.message": error instanceof Error ? error.message : String(error) },
6938
+ {
6939
+ "error.message": error instanceof Error ? error.message : String(error)
6940
+ },
6536
6941
  "Failed to deliver text delta to stream"
6537
6942
  );
6538
6943
  });
@@ -6541,9 +6946,14 @@ async function generateAssistantReply(messageText, context = {}) {
6541
6946
  let newMessages = [];
6542
6947
  try {
6543
6948
  if (resumedFromCheckpoint) {
6544
- const didReplace = await maybeReplaceAgentMessages(agent, existingTurnCheckpoint.piMessages);
6949
+ const didReplace = await maybeReplaceAgentMessages(
6950
+ agent,
6951
+ existingTurnCheckpoint.piMessages
6952
+ );
6545
6953
  if (!didReplace) {
6546
- throw new Error("Agent session resume requested but replaceMessages is unavailable");
6954
+ throw new Error(
6955
+ "Agent session resume requested but replaceMessages is unavailable"
6956
+ );
6547
6957
  }
6548
6958
  }
6549
6959
  beforeMessageCount = agent.state.messages.length;
@@ -6592,10 +7002,16 @@ async function generateAssistantReply(messageText, context = {}) {
6592
7002
  clearTimeout(timeoutId);
6593
7003
  }
6594
7004
  }
6595
- newMessages = agent.state.messages.slice(beforeMessageCount);
7005
+ newMessages = agent.state.messages.slice(
7006
+ beforeMessageCount
7007
+ );
6596
7008
  const outputMessages = newMessages.filter(isAssistantMessage);
6597
7009
  const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
6598
- const usageAttributes = extractGenAiUsageAttributes(promptResult, agent.state, ...outputMessages);
7010
+ const usageAttributes = extractGenAiUsageAttributes(
7011
+ promptResult,
7012
+ agent.state,
7013
+ ...outputMessages
7014
+ );
6599
7015
  setSpanAttributes({
6600
7016
  ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
6601
7017
  ...usageAttributes
@@ -6623,13 +7039,19 @@ async function generateAssistantReply(messageText, context = {}) {
6623
7039
  const toolResults = newMessages.filter(isToolResultMessage);
6624
7040
  const assistantMessages = newMessages.filter(isAssistantMessage);
6625
7041
  const primaryText = assistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
6626
- const toolErrorCount = toolResults.filter((result) => result.isError).length;
7042
+ const toolErrorCount = toolResults.filter(
7043
+ (result) => result.isError
7044
+ ).length;
6627
7045
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
6628
7046
  const successfulToolNames = new Set(
6629
7047
  toolResults.filter((result) => !isToolResultError(result)).map((result) => normalizeToolNameFromResult(result)).filter((value) => Boolean(value))
6630
7048
  );
6631
- const channelPostPerformed = successfulToolNames.has("slackChannelPostMessage");
6632
- const reactionPerformed = successfulToolNames.has("slackMessageAddReaction");
7049
+ const channelPostPerformed = successfulToolNames.has(
7050
+ "slackChannelPostMessage"
7051
+ );
7052
+ const reactionPerformed = successfulToolNames.has(
7053
+ "slackMessageAddReaction"
7054
+ );
6633
7055
  const deliveryPlan = buildReplyDeliveryPlan({
6634
7056
  explicitChannelPostIntent,
6635
7057
  channelPostPerformed,
@@ -6663,7 +7085,10 @@ async function generateAssistantReply(messageText, context = {}) {
6663
7085
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
6664
7086
  const usedPrimaryText = Boolean(primaryText);
6665
7087
  const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : "execution_failure";
6666
- const resolvedText = primaryText || buildExecutionFailureMessage(toolErrorCount);
7088
+ const candidateText = primaryText || buildExecutionFailureMessage(toolErrorCount);
7089
+ const escapedOrRawPayload = isExecutionEscapeResponse(candidateText) || isRawToolPayloadResponse(candidateText);
7090
+ const resolvedText = escapedOrRawPayload ? buildExecutionFailureMessage(toolErrorCount) : enforceAttachmentClaimTruth(candidateText, generatedFiles.length > 0);
7091
+ const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
6667
7092
  if (shouldTrace) {
6668
7093
  logInfo(
6669
7094
  "agent_message_out",
@@ -6672,22 +7097,23 @@ async function generateAssistantReply(messageText, context = {}) {
6672
7097
  "app.message.kind": "assistant_outbound",
6673
7098
  "app.message.length": resolvedText.length,
6674
7099
  "app.message.output": summarizeMessageText(resolvedText),
6675
- "app.ai.outcome": outcome,
7100
+ "app.ai.outcome": resolvedOutcome,
6676
7101
  "app.ai.assistant_messages": assistantMessages.length,
6677
7102
  ...stopReason ? { "app.ai.stop_reason": stopReason } : {}
6678
7103
  },
6679
7104
  "Agent message sent"
6680
7105
  );
6681
7106
  }
6682
- if (isExecutionEscapeResponse(resolvedText) || isRawToolPayloadResponse(resolvedText)) {
7107
+ if (escapedOrRawPayload) {
6683
7108
  return {
6684
- text: buildExecutionFailureMessage(toolErrorCount),
7109
+ text: resolvedText,
6685
7110
  files: generatedFiles.length > 0 ? generatedFiles : void 0,
6686
7111
  artifactStatePatch: Object.keys(artifactStatePatch).length > 0 ? artifactStatePatch : void 0,
6687
7112
  deliveryPlan,
6688
7113
  deliveryMode,
6689
7114
  ackStrategy,
6690
7115
  sandboxId: sandboxExecutor.getSandboxId(),
7116
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
6691
7117
  diagnostics: {
6692
7118
  outcome: "execution_failure",
6693
7119
  modelId: botConfig.modelId,
@@ -6710,6 +7136,7 @@ async function generateAssistantReply(messageText, context = {}) {
6710
7136
  deliveryMode,
6711
7137
  ackStrategy,
6712
7138
  sandboxId: sandboxExecutor.getSandboxId(),
7139
+ sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
6713
7140
  diagnostics: {
6714
7141
  outcome,
6715
7142
  modelId: botConfig.modelId,
@@ -6747,7 +7174,10 @@ async function generateAssistantReply(messageText, context = {}) {
6747
7174
  "Agent turn timed out and will be resumed"
6748
7175
  );
6749
7176
  try {
6750
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(timeoutResumeConversationId, timeoutResumeSessionId);
7177
+ const latestCheckpoint = await getAgentTurnSessionCheckpoint(
7178
+ timeoutResumeConversationId,
7179
+ timeoutResumeSessionId
7180
+ );
6751
7181
  const piMessages = timeoutResumeMessages.length > 0 ? timeoutResumeMessages : latestCheckpoint?.piMessages ?? [];
6752
7182
  await upsertAgentTurnSessionCheckpoint({
6753
7183
  conversationId: timeoutResumeConversationId,
@@ -6787,18 +7217,25 @@ async function generateAssistantReply(messageText, context = {}) {
6787
7217
  if (isRetryableTurnError(error)) {
6788
7218
  throw error;
6789
7219
  }
6790
- logException(error, "assistant_reply_generation_failed", {
6791
- slackThreadId: context.correlation?.threadId,
6792
- slackUserId: context.correlation?.requesterId,
6793
- slackChannelId: context.correlation?.channelId,
6794
- runId: context.correlation?.runId,
6795
- assistantUserName: context.assistant?.userName,
6796
- modelId: botConfig.modelId
6797
- }, {}, "generateAssistantReply failed");
7220
+ logException(
7221
+ error,
7222
+ "assistant_reply_generation_failed",
7223
+ {
7224
+ slackThreadId: context.correlation?.threadId,
7225
+ slackUserId: context.correlation?.requesterId,
7226
+ slackChannelId: context.correlation?.channelId,
7227
+ runId: context.correlation?.runId,
7228
+ assistantUserName: context.assistant?.userName,
7229
+ modelId: botConfig.modelId
7230
+ },
7231
+ {},
7232
+ "generateAssistantReply failed"
7233
+ );
6798
7234
  const message = error instanceof Error ? error.message : String(error);
6799
7235
  return {
6800
7236
  text: `Error: ${message}`,
6801
- sandboxId: void 0,
7237
+ sandboxId: lastKnownSandboxId,
7238
+ sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
6802
7239
  diagnostics: {
6803
7240
  outcome: "provider_error",
6804
7241
  modelId: botConfig.modelId,
@@ -7812,10 +8249,11 @@ function createAppSlackRuntime(deps) {
7812
8249
 
7813
8250
  // src/chat/app-home.ts
7814
8251
  import fs5 from "fs";
7815
- import path4 from "path";
8252
+ import path5 from "path";
7816
8253
  var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
7817
8254
  var MAX_HOME_SKILLS = 6;
7818
8255
  var MAX_SECTION_TEXT_CHARS = 3e3;
8256
+ var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
7819
8257
  function clampSectionText(text) {
7820
8258
  if (text.length <= MAX_SECTION_TEXT_CHARS) {
7821
8259
  return text;
@@ -7823,7 +8261,7 @@ function clampSectionText(text) {
7823
8261
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
7824
8262
  }
7825
8263
  function loadAboutText() {
7826
- const aboutPath = path4.join(homeDir(), "ABOUT.md");
8264
+ const aboutPath = path5.join(homeDir(), "ABOUT.md");
7827
8265
  try {
7828
8266
  const raw = fs5.readFileSync(aboutPath, "utf8").trim();
7829
8267
  if (raw.length > 0) {
@@ -7834,12 +8272,12 @@ function loadAboutText() {
7834
8272
  return DEFAULT_ABOUT_TEXT;
7835
8273
  }
7836
8274
  async function buildSkillsSummaryText() {
7837
- const skills = await discoverSkills();
8275
+ const skills = (await discoverSkills()).filter((skill) => !HIDDEN_HOME_SKILLS.has(skill.name));
7838
8276
  if (skills.length === 0) {
7839
8277
  return "No skills installed.";
7840
8278
  }
7841
8279
  const visible = skills.slice(0, MAX_HOME_SKILLS);
7842
- const lines = visible.map((skill) => `\u2022 \`/${skill.name}\` \u2014 ${skill.description}`);
8280
+ const lines = visible.map((skill) => `\u2022 \`!${skill.name}\` \u2014 ${skill.description}`);
7843
8281
  if (skills.length > visible.length) {
7844
8282
  lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
7845
8283
  }
@@ -7939,9 +8377,6 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
7939
8377
  }
7940
8378
 
7941
8379
  // src/chat/slash-command.ts
7942
- function providerLabel(provider) {
7943
- return provider.charAt(0).toUpperCase() + provider.slice(1);
7944
- }
7945
8380
  async function postEphemeral(event, text) {
7946
8381
  await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
7947
8382
  }
@@ -7950,10 +8385,10 @@ async function handleLink(event, provider) {
7950
8385
  await postEphemeral(event, `Unknown provider: \`${provider}\``);
7951
8386
  return;
7952
8387
  }
7953
- if (!getOAuthProviderConfig(provider)) {
8388
+ if (!getPluginOAuthConfig(provider)) {
7954
8389
  await postEphemeral(
7955
8390
  event,
7956
- `${providerLabel(provider)} doesn't support account linking.`
8391
+ `${formatProviderLabel(provider)} doesn't support account linking.`
7957
8392
  );
7958
8393
  return;
7959
8394
  }
@@ -7967,7 +8402,10 @@ async function handleLink(event, provider) {
7967
8402
  return;
7968
8403
  }
7969
8404
  if (result.delivery === "fallback_dm") {
7970
- await postEphemeral(event, `Check your DMs for a ${providerLabel(provider)} authorization link.`);
8405
+ await postEphemeral(
8406
+ event,
8407
+ `Check your DMs for a ${formatProviderLabel(provider)} authorization link.`
8408
+ );
7971
8409
  } else if (result.delivery === false) {
7972
8410
  await postEphemeral(
7973
8411
  event,
@@ -7980,10 +8418,10 @@ async function handleUnlink(event, provider) {
7980
8418
  await postEphemeral(event, `Unknown provider: \`${provider}\``);
7981
8419
  return;
7982
8420
  }
7983
- if (!getOAuthProviderConfig(provider)) {
8421
+ if (!getPluginOAuthConfig(provider)) {
7984
8422
  await postEphemeral(
7985
8423
  event,
7986
- `${providerLabel(provider)} doesn't support account unlinking.`
8424
+ `${formatProviderLabel(provider)} doesn't support account unlinking.`
7987
8425
  );
7988
8426
  return;
7989
8427
  }
@@ -7993,14 +8431,20 @@ async function handleUnlink(event, provider) {
7993
8431
  "slash_command_unlink",
7994
8432
  { slackUserId: event.user.userId },
7995
8433
  { "app.credential.provider": provider },
7996
- `Unlinked ${providerLabel(provider)} account via /jr slash command`
8434
+ `Unlinked ${formatProviderLabel(provider)} account via /jr slash command`
8435
+ );
8436
+ await postEphemeral(
8437
+ event,
8438
+ `Your ${formatProviderLabel(provider)} account has been unlinked.`
7997
8439
  );
7998
- await postEphemeral(event, `Your ${providerLabel(provider)} account has been unlinked.`);
7999
8440
  }
8000
8441
  async function handleSlashCommand(event) {
8001
8442
  const [subcommand, provider, ...rest] = event.text.trim().split(/\s+/);
8002
8443
  if (!subcommand || !["link", "unlink"].includes(subcommand)) {
8003
- await postEphemeral(event, "Usage: `/jr link <provider>` or `/jr unlink <provider>`");
8444
+ await postEphemeral(
8445
+ event,
8446
+ "Usage: `/jr link <provider>` or `/jr unlink <provider>`"
8447
+ );
8004
8448
  return;
8005
8449
  }
8006
8450
  if (!provider || rest.length > 0) {
@@ -8359,6 +8803,9 @@ async function persistThreadState(thread, patch) {
8359
8803
  if (patch.sandboxId) {
8360
8804
  payload.app_sandbox_id = patch.sandboxId;
8361
8805
  }
8806
+ if (patch.sandboxDependencyProfileHash) {
8807
+ payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
8808
+ }
8362
8809
  if (Object.keys(payload).length === 0) {
8363
8810
  return;
8364
8811
  }
@@ -9203,7 +9650,9 @@ async function hydrateConversationVisionContext(conversation, context) {
9203
9650
 
9204
9651
  // src/chat/turn/execute.ts
9205
9652
  function resolveReplyDelivery(args) {
9206
- const replyHasFiles = Boolean(args.reply.files && args.reply.files.length > 0);
9653
+ const replyHasFiles = Boolean(
9654
+ args.reply.files && args.reply.files.length > 0
9655
+ );
9207
9656
  const deliveryPlan = args.reply.deliveryPlan ?? {
9208
9657
  mode: args.reply.deliveryMode ?? "thread",
9209
9658
  ack: args.reply.ackStrategy ?? "none",
@@ -9214,8 +9663,9 @@ function resolveReplyDelivery(args) {
9214
9663
  if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
9215
9664
  attachFiles = "inline";
9216
9665
  }
9666
+ const suppressRedundantReactionReply = deliveryPlan.ack === "reaction" && !replyHasFiles && isRedundantReactionAckText(args.reply.text);
9217
9667
  return {
9218
- shouldPostThreadReply: deliveryPlan.postThreadText,
9668
+ shouldPostThreadReply: deliveryPlan.postThreadText && !suppressRedundantReactionReply,
9219
9669
  attachFiles
9220
9670
  };
9221
9671
  }
@@ -9279,7 +9729,9 @@ function createReplyToThread(deps) {
9279
9729
  thread,
9280
9730
  message,
9281
9731
  userText,
9282
- explicitMention: Boolean(options.explicitMention || message.isMention),
9732
+ explicitMention: Boolean(
9733
+ options.explicitMention || message.isMention
9734
+ ),
9283
9735
  context: {
9284
9736
  threadId,
9285
9737
  requesterId: message.author.userId,
@@ -9324,17 +9776,22 @@ function createReplyToThread(deps) {
9324
9776
  await persistThreadState(thread, {
9325
9777
  conversation: preparedState.conversation
9326
9778
  });
9327
- const fallbackIdentity = await getBotDeps().lookupSlackUser(message.author.userId);
9779
+ const fallbackIdentity = await getBotDeps().lookupSlackUser(
9780
+ message.author.userId
9781
+ );
9328
9782
  const resolvedUserName = message.author.userName ?? fallbackIdentity?.userName;
9329
9783
  if (resolvedUserName) {
9330
9784
  setTags({ slackUserName: resolvedUserName });
9331
9785
  }
9332
- const userAttachments = await resolveUserAttachments(message.attachments, {
9333
- threadId,
9334
- requesterId: message.author.userId,
9335
- channelId,
9336
- runId
9337
- });
9786
+ const userAttachments = await resolveUserAttachments(
9787
+ message.attachments,
9788
+ {
9789
+ threadId,
9790
+ requesterId: message.author.userId,
9791
+ channelId,
9792
+ runId
9793
+ }
9794
+ );
9338
9795
  const progress = createProgressReporter({
9339
9796
  channelId,
9340
9797
  threadTs,
@@ -9342,6 +9799,7 @@ function createReplyToThread(deps) {
9342
9799
  });
9343
9800
  const textStream = createTextStreamBridge();
9344
9801
  let streamedReplyPromise;
9802
+ let pendingStreamText = "";
9345
9803
  let beforeFirstResponsePostCalled = false;
9346
9804
  const beforeFirstResponsePost = async () => {
9347
9805
  if (beforeFirstResponsePostCalled) {
@@ -9354,13 +9812,24 @@ function createReplyToThread(deps) {
9354
9812
  if (!streamedReplyPromise) {
9355
9813
  const streamingReply = (async () => {
9356
9814
  return await postThreadReply(
9357
- createNormalizingStream(textStream.iterable, ensureBlockSpacing),
9815
+ createNormalizingStream(
9816
+ textStream.iterable,
9817
+ ensureBlockSpacing
9818
+ ),
9358
9819
  "streaming_initial_post"
9359
9820
  );
9360
9821
  })();
9361
9822
  streamedReplyPromise = streamingReply;
9362
9823
  }
9363
9824
  };
9825
+ const flushPendingStreamText = () => {
9826
+ if (!pendingStreamText) {
9827
+ return;
9828
+ }
9829
+ startStreamingReply();
9830
+ textStream.push(pendingStreamText);
9831
+ pendingStreamText = "";
9832
+ };
9364
9833
  const postThreadReply = async (payload, stage) => {
9365
9834
  await beforeFirstResponsePost();
9366
9835
  try {
@@ -9411,17 +9880,28 @@ function createReplyToThread(deps) {
9411
9880
  },
9412
9881
  toolChannelId,
9413
9882
  sandbox: {
9414
- sandboxId: preparedState.sandboxId
9883
+ sandboxId: preparedState.sandboxId,
9884
+ sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
9415
9885
  },
9416
9886
  onStatus: (status) => progress.setStatus(status),
9417
9887
  onTextDelta: (deltaText) => {
9418
9888
  if (explicitChannelPostIntent) {
9419
9889
  return;
9420
9890
  }
9421
- startStreamingReply();
9422
- textStream.push(deltaText);
9891
+ if (streamedReplyPromise) {
9892
+ textStream.push(deltaText);
9893
+ return;
9894
+ }
9895
+ pendingStreamText += deltaText;
9896
+ if (isPotentialRedundantReactionAckText(pendingStreamText)) {
9897
+ return;
9898
+ }
9899
+ flushPendingStreamText();
9423
9900
  }
9424
9901
  });
9902
+ if (streamedReplyPromise) {
9903
+ flushPendingStreamText();
9904
+ }
9425
9905
  textStream.end();
9426
9906
  const diagnosticsContext = {
9427
9907
  slackThreadId: threadId,
@@ -9445,7 +9925,9 @@ function createReplyToThread(deps) {
9445
9925
  };
9446
9926
  setSpanAttributes(diagnosticsAttributes);
9447
9927
  if (reply.diagnostics.outcome === "provider_error") {
9448
- const providerError = reply.diagnostics.providerError ?? new Error(reply.diagnostics.errorMessage ?? "Provider error without explicit message");
9928
+ const providerError = reply.diagnostics.providerError ?? new Error(
9929
+ reply.diagnostics.errorMessage ?? "Provider error without explicit message"
9930
+ );
9449
9931
  logException(
9450
9932
  providerError,
9451
9933
  "agent_turn_provider_error",
@@ -9461,10 +9943,14 @@ function createReplyToThread(deps) {
9461
9943
  "Agent turn completed with execution failure"
9462
9944
  );
9463
9945
  }
9464
- markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
9465
- replied: true,
9466
- skippedReason: void 0
9467
- });
9946
+ markConversationMessage(
9947
+ preparedState.conversation,
9948
+ preparedState.userMessageId,
9949
+ {
9950
+ replied: true,
9951
+ skippedReason: void 0
9952
+ }
9953
+ );
9468
9954
  upsertConversationMessage(preparedState.conversation, {
9469
9955
  id: generateConversationId("assistant"),
9470
9956
  role: "assistant",
@@ -9487,15 +9973,19 @@ function createReplyToThread(deps) {
9487
9973
  if (shouldPostThreadReply) {
9488
9974
  if (!streamedReplyPromise) {
9489
9975
  await postThreadReply(
9490
- buildSlackOutputMessage(reply.text, {
9491
- files: resolvedAttachFiles === "inline" ? replyFiles : void 0
9492
- }),
9976
+ buildSlackOutputMessage(
9977
+ reply.text,
9978
+ resolvedAttachFiles === "inline" ? replyFiles : void 0
9979
+ ),
9493
9980
  "thread_reply"
9494
9981
  );
9495
9982
  } else {
9496
9983
  await streamedReplyPromise;
9497
9984
  if (reply.diagnostics.outcome !== "success" && reply.text.trim().length > 0) {
9498
- await postThreadReply(buildSlackOutputMessage(reply.text), "thread_reply_after_stream_failure");
9985
+ await postThreadReply(
9986
+ buildSlackOutputMessage(reply.text),
9987
+ "thread_reply_after_stream_failure"
9988
+ );
9499
9989
  }
9500
9990
  }
9501
9991
  }
@@ -9509,7 +9999,8 @@ function createReplyToThread(deps) {
9509
9999
  await persistThreadState(thread, {
9510
10000
  artifacts: nextArtifacts,
9511
10001
  conversation: preparedState.conversation,
9512
- sandboxId: reply.sandboxId
10002
+ sandboxId: reply.sandboxId,
10003
+ sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
9513
10004
  });
9514
10005
  persistedAtLeastOnce = true;
9515
10006
  if (shouldEmitDevAgentTrace()) {
@@ -9525,9 +10016,13 @@ function createReplyToThread(deps) {
9525
10016
  "Agent turn completed"
9526
10017
  );
9527
10018
  }
9528
- const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter((m) => m.role === "assistant").length === 1;
10019
+ const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
10020
+ (m) => m.role === "assistant"
10021
+ ).length === 1;
9529
10022
  if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
9530
- void generateThreadTitle(userText, reply.text).then((title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)).catch((error) => {
10023
+ void generateThreadTitle(userText, reply.text).then(
10024
+ (title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)
10025
+ ).catch((error) => {
9531
10026
  const slackErrorCode = getSlackApiErrorCode(error);
9532
10027
  const assistantTitleErrorAttributes = {
9533
10028
  "app.slack.assistant_title.outcome": "permission_denied",
@@ -9560,14 +10055,16 @@ function createReplyToThread(deps) {
9560
10055
  assistantUserName: botConfig.userName,
9561
10056
  modelId: botConfig.fastModelId
9562
10057
  },
9563
- { "error.message": error instanceof Error ? error.message : String(error) },
10058
+ {
10059
+ "error.message": error instanceof Error ? error.message : String(error)
10060
+ },
9564
10061
  "Thread title generation failed"
9565
10062
  );
9566
10063
  });
9567
10064
  }
9568
10065
  if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
9569
10066
  await postThreadReply(
9570
- { files: replyFiles },
10067
+ buildSlackOutputMessage("", replyFiles),
9571
10068
  "thread_reply_files_followup"
9572
10069
  );
9573
10070
  }
@@ -9610,7 +10107,12 @@ function createReplyToThread(deps) {
9610
10107
  // src/chat/runtime/turn-preparation.ts
9611
10108
  async function prepareTurnState(args) {
9612
10109
  const existingState = await args.thread.state;
9613
- const existingSandboxId = existingState ? toOptionalString(existingState.app_sandbox_id) : void 0;
10110
+ const existingSandboxId = existingState ? toOptionalString(
10111
+ existingState.app_sandbox_id
10112
+ ) : void 0;
10113
+ const existingSandboxDependencyProfileHash = existingState ? toOptionalString(
10114
+ existingState.app_sandbox_dependency_profile_hash
10115
+ ) : void 0;
9614
10116
  const artifacts = coerceThreadArtifactsState(existingState);
9615
10117
  const conversation = coerceThreadConversationState(existingState);
9616
10118
  const channelConfiguration = getChannelConfigurationService(args.thread);
@@ -9619,13 +10121,15 @@ async function prepareTurnState(args) {
9619
10121
  messageId: args.message.id,
9620
10122
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
9621
10123
  });
9622
- const messageHasPotentialImageAttachment = args.message.attachments.some((attachment) => {
9623
- if (attachment.type === "image") {
9624
- return true;
10124
+ const messageHasPotentialImageAttachment = args.message.attachments.some(
10125
+ (attachment) => {
10126
+ if (attachment.type === "image") {
10127
+ return true;
10128
+ }
10129
+ const mimeType = attachment.mimeType ?? "";
10130
+ return attachment.type === "file" && mimeType.startsWith("image/");
9625
10131
  }
9626
- const mimeType = attachment.mimeType ?? "";
9627
- return attachment.type === "file" && mimeType.startsWith("image/");
9628
- });
10132
+ );
9629
10133
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
9630
10134
  const incomingUserMessage = {
9631
10135
  id: args.message.id,
@@ -9644,7 +10148,10 @@ async function prepareTurnState(args) {
9644
10148
  imagesHydrated: !messageHasPotentialImageAttachment
9645
10149
  }
9646
10150
  };
9647
- const userMessageId = upsertConversationMessage(conversation, incomingUserMessage);
10151
+ const userMessageId = upsertConversationMessage(
10152
+ conversation,
10153
+ incomingUserMessage
10154
+ );
9648
10155
  if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
9649
10156
  await hydrateConversationVisionContext(conversation, {
9650
10157
  threadId: args.context.threadId,
@@ -9674,6 +10181,7 @@ async function prepareTurnState(args) {
9674
10181
  channelConfiguration,
9675
10182
  conversation,
9676
10183
  sandboxId: existingSandboxId,
10184
+ sandboxDependencyProfileHash: existingSandboxDependencyProfileHash,
9677
10185
  conversationContext,
9678
10186
  routingContext,
9679
10187
  userMessageId
@@ -9766,7 +10274,7 @@ export {
9766
10274
  getUserTokenStore,
9767
10275
  getSlackClient,
9768
10276
  downloadPrivateSlackFile,
9769
- getOAuthProviderConfig,
10277
+ formatProviderLabel,
9770
10278
  resolveBaseUrl,
9771
10279
  escapeXml,
9772
10280
  removeReactionFromMessage,