@sentry/junior 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  findSkillByName,
4
4
  loadSkillsByName,
5
5
  parseSkillInvocation
6
- } from "./chunk-ZUUJTQ2H.js";
6
+ } from "./chunk-AYM42AN3.js";
7
7
  import {
8
8
  GEN_AI_PROVIDER_NAME,
9
9
  MISSING_GATEWAY_CREDENTIALS_ERROR,
@@ -31,7 +31,7 @@ import {
31
31
  runNonInteractiveCommand,
32
32
  sandboxSkillDir,
33
33
  sandboxSkillFile
34
- } from "./chunk-ELM6HJ6S.js";
34
+ } from "./chunk-AQ4RO2WA.js";
35
35
  import {
36
36
  CredentialUnavailableError,
37
37
  buildOAuthTokenRequest,
@@ -61,6 +61,7 @@ import {
61
61
  resolveAuthTokenPlaceholder,
62
62
  resolvePluginCommandEnv,
63
63
  serializeGenAiAttribute,
64
+ setPluginConfig,
64
65
  setSpanAttributes,
65
66
  setSpanStatus,
66
67
  setTags,
@@ -68,7 +69,7 @@ import {
68
69
  toOptionalString,
69
70
  withContext,
70
71
  withSpan
71
- } from "./chunk-BCG3I2T2.js";
72
+ } from "./chunk-UKR24HLJ.js";
72
73
  import {
73
74
  sentry_exports
74
75
  } from "./chunk-Z3YD6NHK.js";
@@ -2448,6 +2449,12 @@ import { Agent as Agent2 } from "@mariozechner/pi-agent-core";
2448
2449
  import fs from "fs";
2449
2450
  import path2 from "path";
2450
2451
 
2452
+ // src/chat/interruption-marker.ts
2453
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2454
+ function getInterruptionMarker() {
2455
+ return INTERRUPTED_MARKER;
2456
+ }
2457
+
2451
2458
  // src/chat/slack/status-format.ts
2452
2459
  var SLACK_STATUS_MAX_LENGTH = 50;
2453
2460
  function truncateStatusText(text) {
@@ -2511,7 +2518,6 @@ function normalizeSlackStatusText(text) {
2511
2518
  var MAX_INLINE_CHARS = 2200;
2512
2519
  var MAX_INLINE_LINES = 45;
2513
2520
  var CONTINUED_MARKER = "\n\n[Continued below]";
2514
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2515
2521
  function countSlackLines(text) {
2516
2522
  if (!text) {
2517
2523
  return 0;
@@ -2678,10 +2684,10 @@ function splitSlackReplyText(text, options) {
2678
2684
  const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2679
2685
  let remaining = normalized;
2680
2686
  while (remaining) {
2681
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, INTERRUPTED_MARKER)) : fitsInlineBudget(remaining);
2687
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
2682
2688
  if (fitsFinalChunk) {
2683
2689
  chunks.push(
2684
- options?.interrupted ? appendSlackSuffix(remaining, INTERRUPTED_MARKER) : remaining
2690
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
2685
2691
  );
2686
2692
  break;
2687
2693
  }
@@ -2831,26 +2837,50 @@ function renderTag(tag, lines) {
2831
2837
  function renderTagBlock(tag, content) {
2832
2838
  return [`<${tag}>`, content, `</${tag}>`].join("\n");
2833
2839
  }
2834
- function formatAvailableSkillsForPrompt(skills) {
2835
- if (skills.length === 0) {
2836
- return "<available-skills>\n</available-skills>";
2837
- }
2838
- const lines = ["<available-skills>"];
2839
- for (const skill of skills) {
2840
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
2841
- lines.push(" <skill>");
2842
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
2843
- lines.push(
2844
- ` <description>${escapeXml(skill.description)}</description>`
2845
- );
2846
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
2847
- if (skill.pluginProvider) {
2848
- lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
2840
+ function formatSkillEntry(skill) {
2841
+ const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
2842
+ const lines = [];
2843
+ lines.push(" <skill>");
2844
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
2845
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
2846
+ lines.push(` <location>${escapeXml(skillLocation)}</location>`);
2847
+ if (skill.pluginProvider) {
2848
+ lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
2849
+ }
2850
+ lines.push(" </skill>");
2851
+ return lines;
2852
+ }
2853
+ function formatAvailableSkillsForPrompt(skills, invocation) {
2854
+ const autoSelectable = skills.filter(
2855
+ (s) => s.disableModelInvocation !== true
2856
+ );
2857
+ const invokedExplicitOnly = invocation ? skills.filter(
2858
+ (s) => s.disableModelInvocation === true && s.name === invocation.skillName
2859
+ ) : [];
2860
+ const sections = [];
2861
+ const available = [
2862
+ "<available-skills>",
2863
+ ...autoSelectable.length > 0 ? [
2864
+ "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. If none fits, do not load a skill."
2865
+ ] : []
2866
+ ];
2867
+ for (const skill of autoSelectable) {
2868
+ available.push(...formatSkillEntry(skill));
2869
+ }
2870
+ available.push("</available-skills>");
2871
+ sections.push(available.join("\n"));
2872
+ if (invokedExplicitOnly.length > 0) {
2873
+ const userCallable = [
2874
+ "<user-callable-skills>",
2875
+ "The user's current message explicitly references this skill by name. Load it when relevant to the request."
2876
+ ];
2877
+ for (const skill of invokedExplicitOnly) {
2878
+ userCallable.push(...formatSkillEntry(skill));
2849
2879
  }
2850
- lines.push(" </skill>");
2880
+ userCallable.push("</user-callable-skills>");
2881
+ sections.push(userCallable.join("\n"));
2851
2882
  }
2852
- lines.push("</available-skills>");
2853
- return lines.join("\n");
2883
+ return sections.join("\n");
2854
2884
  }
2855
2885
  function formatLoadedSkillsForPrompt(skills) {
2856
2886
  if (skills.length === 0) {
@@ -2878,7 +2908,7 @@ function formatProviderCatalogForPrompt() {
2878
2908
  return null;
2879
2909
  }
2880
2910
  const lines = [
2881
- "Config keys and default targets per provider; use after a skill is loaded."
2911
+ "Config keys and default targets per provider; use after a skill is loaded. Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes."
2882
2912
  ];
2883
2913
  for (const provider of providers) {
2884
2914
  lines.push(`- provider: ${escapeXml(provider.name)}`);
@@ -2898,7 +2928,7 @@ function formatActiveMcpCatalogsForPrompt(catalogs) {
2898
2928
  return null;
2899
2929
  }
2900
2930
  const lines = [
2901
- "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`."
2931
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
2902
2932
  ];
2903
2933
  for (const catalog of catalogs) {
2904
2934
  lines.push(" <catalog>");
@@ -3008,12 +3038,8 @@ var TOOL_CALL_STYLE_RULES = [
3008
3038
  "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
3009
3039
  ];
3010
3040
  var SKILL_POLICY_RULES = [
3011
- "- Before answering, scan `<available-skills>`. For matching operational or conceptual provider/repository workflow questions, load the most specific skill; do not answer from memory first. If none fits, do not load a skill.",
3012
- "- Never load multiple skills up front. After `loadSkill`, follow `<loaded-skills>` and resolve relative references under that skill's location.",
3013
- "- For explicit `/skill` triggers, treat that skill as selected unless the tool says it is unavailable.",
3014
- "- For active MCP catalogs, use `searchMcpTools` to inspect descriptors before `callMcpTool`; pass exact returned `tool_name` values and put provider fields inside `arguments`.",
3015
- "- Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes.",
3016
- "- Run `jr-rpc config get|set|unset|list` as standalone bash commands for conversation-scoped provider defaults; do not chain them with `cd`, `&&`, pipes, or provider commands."
3041
+ "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
3042
+ "- Load one skill at a time. After `loadSkill`, follow the instructions in `<loaded-skills>`."
3017
3043
  ];
3018
3044
  var EXECUTION_CONTRACT_RULES = [
3019
3045
  "- Actionable request: act in this turn.",
@@ -3026,7 +3052,7 @@ var EXECUTION_CONTRACT_RULES = [
3026
3052
  var CONVERSATION_RULES = [
3027
3053
  "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
3028
3054
  "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
3029
- "- On resumed turns, post a brief continuation notice, then the resumed answer as a separate message."
3055
+ "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
3030
3056
  ];
3031
3057
  var SLACK_ACTION_RULES = [
3032
3058
  "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
@@ -3123,7 +3149,7 @@ function buildContextSection(params) {
3123
3149
  if (configLines) {
3124
3150
  blocks.push(
3125
3151
  renderTag("configuration", [
3126
- "Ambient provider defaults; explicit targets win.",
3152
+ "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
3127
3153
  ...configLines
3128
3154
  ])
3129
3155
  );
@@ -3135,16 +3161,21 @@ function buildContextSection(params) {
3135
3161
  ]);
3136
3162
  }
3137
3163
  if (params.invocation) {
3138
- blocks.push([
3139
- `<explicit-skill-trigger>/${escapeXml(params.invocation.skillName)}</explicit-skill-trigger>`
3140
- ]);
3164
+ blocks.push(
3165
+ renderTag("explicit-skill-trigger", [
3166
+ "Treat this skill as selected. Load it unless the tool says it is unavailable.",
3167
+ `/${escapeXml(params.invocation.skillName)}`
3168
+ ])
3169
+ );
3141
3170
  }
3142
3171
  const body = blocks.map((block) => block.join("\n")).join("\n\n");
3143
3172
  return renderTagBlock("context", body);
3144
3173
  }
3145
3174
  function buildCapabilitiesSection(params) {
3146
3175
  const blocks = [];
3147
- blocks.push(formatAvailableSkillsForPrompt(params.availableSkills));
3176
+ blocks.push(
3177
+ formatAvailableSkillsForPrompt(params.availableSkills, params.invocation)
3178
+ );
3148
3179
  blocks.push(formatLoadedSkillsForPrompt(params.activeSkills));
3149
3180
  const activeCatalogs = formatActiveMcpCatalogsForPrompt(
3150
3181
  params.activeMcpCatalogs
@@ -3181,6 +3212,7 @@ function buildTurnContextPrompt(params) {
3181
3212
  availableSkills: params.availableSkills,
3182
3213
  activeSkills: params.activeSkills,
3183
3214
  activeMcpCatalogs: params.activeMcpCatalogs ?? [],
3215
+ invocation: params.invocation,
3184
3216
  toolGuidance: params.toolGuidance ?? []
3185
3217
  }),
3186
3218
  buildContextSection({
@@ -4442,6 +4474,16 @@ function createBashTool() {
4442
4474
 
4443
4475
  // src/chat/tools/sandbox/file-utils.ts
4444
4476
  import path4 from "path";
4477
+
4478
+ // src/chat/tools/execution/tool-input-error.ts
4479
+ var ToolInputError = class extends Error {
4480
+ constructor(message, options) {
4481
+ super(message, options);
4482
+ this.name = "ToolInputError";
4483
+ }
4484
+ };
4485
+
4486
+ // src/chat/tools/sandbox/file-utils.ts
4445
4487
  var MAX_TEXT_CHARS = 6e4;
4446
4488
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
4447
4489
  function positiveInteger(value) {
@@ -4534,7 +4576,7 @@ function resolveWorkspacePath(input, fallback = ".") {
4534
4576
  const absolute = requested.startsWith("/") ? requested : path4.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
4535
4577
  const normalized = path4.posix.normalize(absolute);
4536
4578
  if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
4537
- throw new Error(
4579
+ throw new ToolInputError(
4538
4580
  `Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
4539
4581
  );
4540
4582
  }
@@ -4733,16 +4775,16 @@ function buildCompactDiff(oldContent, newContent) {
4733
4775
  }
4734
4776
  function validateAndApplyTextEdits(content, edits, targetName) {
4735
4777
  if (!Array.isArray(edits) || edits.length === 0) {
4736
- throw new Error(`${targetName} requires at least one edit.`);
4778
+ throw new ToolInputError(`${targetName} requires at least one edit.`);
4737
4779
  }
4738
4780
  const normalizedEdits = edits.map((edit, index) => {
4739
4781
  if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
4740
- throw new Error(
4782
+ throw new ToolInputError(
4741
4783
  `edits[${index}].oldText must not be empty in ${targetName}.`
4742
4784
  );
4743
4785
  }
4744
4786
  if (typeof edit.newText !== "string") {
4745
- throw new Error(
4787
+ throw new ToolInputError(
4746
4788
  `edits[${index}].newText must be a string in ${targetName}.`
4747
4789
  );
4748
4790
  }
@@ -4756,13 +4798,13 @@ function validateAndApplyTextEdits(content, edits, targetName) {
4756
4798
  const edit = normalizedEdits[index];
4757
4799
  const matchIndex = content.indexOf(edit.oldText);
4758
4800
  if (matchIndex === -1) {
4759
- throw new Error(
4801
+ throw new ToolInputError(
4760
4802
  `Could not find edits[${index}] in ${targetName}. oldText must match exactly including whitespace and newlines.`
4761
4803
  );
4762
4804
  }
4763
4805
  const occurrences = countOccurrences(content, edit.oldText);
4764
4806
  if (occurrences > 1) {
4765
- throw new Error(
4807
+ throw new ToolInputError(
4766
4808
  `Found ${occurrences} occurrences of edits[${index}] in ${targetName}. Each oldText must be unique.`
4767
4809
  );
4768
4810
  }
@@ -4778,7 +4820,7 @@ function validateAndApplyTextEdits(content, edits, targetName) {
4778
4820
  const previous = matchedEdits[index - 1];
4779
4821
  const current = matchedEdits[index];
4780
4822
  if (previous.matchIndex + previous.matchLength > current.matchIndex) {
4781
- throw new Error(
4823
+ throw new ToolInputError(
4782
4824
  `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${targetName}. Merge overlapping replacements into one edit.`
4783
4825
  );
4784
4826
  }
@@ -4789,7 +4831,7 @@ function validateAndApplyTextEdits(content, edits, targetName) {
4789
4831
  newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
4790
4832
  }
4791
4833
  if (newContent === content) {
4792
- throw new Error(`No changes made to ${targetName}.`);
4834
+ throw new ToolInputError(`No changes made to ${targetName}.`);
4793
4835
  }
4794
4836
  return { baseContent: content, newContent };
4795
4837
  }
@@ -4801,7 +4843,17 @@ function prepareEditFileArguments(input) {
4801
4843
  }
4802
4844
  async function editFile(params) {
4803
4845
  const filePath = resolveWorkspacePath(params.path);
4804
- const rawContent = await params.fs.readFile(filePath, { encoding: "utf8" });
4846
+ let rawContent;
4847
+ try {
4848
+ rawContent = await params.fs.readFile(filePath, { encoding: "utf8" });
4849
+ } catch (error) {
4850
+ if (isMissingPathError(error)) {
4851
+ throw new ToolInputError(`File not found: ${params.path}`, {
4852
+ cause: error
4853
+ });
4854
+ }
4855
+ throw error;
4856
+ }
4805
4857
  const { bom, text } = stripBom(rawContent);
4806
4858
  const lineEnding = detectLineEnding(text);
4807
4859
  const normalizedContent = normalizeToLf(text);
@@ -4993,7 +5045,16 @@ async function grepFiles(params) {
4993
5045
  const root = resolveWorkspacePath(params.path);
4994
5046
  const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
4995
5047
  const context = positiveInteger(params.context) ?? 0;
4996
- const regex = params.literal ? void 0 : new RegExp(params.pattern, params.ignoreCase ? "i" : "");
5048
+ let regex;
5049
+ if (!params.literal) {
5050
+ try {
5051
+ regex = new RegExp(params.pattern, params.ignoreCase ? "i" : "");
5052
+ } catch (error) {
5053
+ throw new ToolInputError(`Invalid regex pattern: ${params.pattern}`, {
5054
+ cause: error
5055
+ });
5056
+ }
5057
+ }
4997
5058
  const { files, missingPath, missingRoot } = await collectFiles({
4998
5059
  fs: params.fs,
4999
5060
  root,
@@ -5311,7 +5372,7 @@ async function listDir(params) {
5311
5372
  throw error;
5312
5373
  }
5313
5374
  if (!stat.isDirectory()) {
5314
- throw new Error(`Not a directory: ${params.path ?? "."}`);
5375
+ throw new ToolInputError(`Not a directory: ${params.path ?? "."}`);
5315
5376
  }
5316
5377
  let entries;
5317
5378
  try {
@@ -8220,7 +8281,7 @@ var FETCH_TIMEOUT_MS = 8e3;
8220
8281
  var MAX_REDIRECTS = 3;
8221
8282
  var DEFAULT_MAX_CHARS = 6e3;
8222
8283
  var MAX_FETCH_CHARS = 12e3;
8223
- var MAX_FETCH_BYTES = 256e3;
8284
+ var MAX_FETCH_BYTES = 1e6;
8224
8285
 
8225
8286
  // src/chat/tools/web/network.ts
8226
8287
  import dns from "dns/promises";
@@ -8488,37 +8549,120 @@ function normalizeWhitespace(text) {
8488
8549
  }
8489
8550
  function truncateAtWordBoundary(text, maxChars) {
8490
8551
  if (text.length <= maxChars) {
8491
- return text;
8552
+ return { content: text, truncated: false };
8492
8553
  }
8493
8554
  const shortened = text.slice(0, maxChars);
8494
8555
  const lastSpace = shortened.lastIndexOf(" ");
8495
8556
  if (lastSpace > maxChars * 0.8) {
8496
- return `${shortened.slice(0, lastSpace).trimEnd()}...`;
8557
+ return {
8558
+ content: `${shortened.slice(0, lastSpace).trimEnd()}...`,
8559
+ truncated: true
8560
+ };
8561
+ }
8562
+ return { content: `${shortened.trimEnd()}...`, truncated: true };
8563
+ }
8564
+ function decodeHtmlEntities(value) {
8565
+ return value.replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
8566
+ }
8567
+ function extractTitle(html) {
8568
+ const match = html.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i);
8569
+ const title = match ? normalizeWhitespace(decodeHtmlEntities(match[1])) : "";
8570
+ return title.length > 0 ? title : void 0;
8571
+ }
8572
+ function escapeRegex(value) {
8573
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8574
+ }
8575
+ function getBalancedElementHtml(args) {
8576
+ const tagPattern = new RegExp(
8577
+ `</?${escapeRegex(args.tagName)}\\b[^>]*>`,
8578
+ "gi"
8579
+ );
8580
+ tagPattern.lastIndex = args.startIndex;
8581
+ let depth = 0;
8582
+ for (const match of args.html.matchAll(tagPattern)) {
8583
+ const tag = match[0];
8584
+ if (tag.startsWith("</")) {
8585
+ depth -= 1;
8586
+ if (depth === 0) {
8587
+ return args.html.slice(args.startIndex, match.index + tag.length);
8588
+ }
8589
+ continue;
8590
+ }
8591
+ if (!tag.endsWith("/>")) {
8592
+ depth += 1;
8593
+ }
8594
+ }
8595
+ return void 0;
8596
+ }
8597
+ function findElementHtml(html, predicate) {
8598
+ const openingTagPattern = /<([a-z][\w:-]*)\b[^>]*>/gi;
8599
+ for (const match of html.matchAll(openingTagPattern)) {
8600
+ const tagName = match[1];
8601
+ if (!tagName || !predicate(tagName.toLowerCase(), match[0])) {
8602
+ continue;
8603
+ }
8604
+ const balanced = getBalancedElementHtml({
8605
+ html,
8606
+ startIndex: match.index,
8607
+ tagName
8608
+ });
8609
+ if (balanced) {
8610
+ return balanced;
8611
+ }
8497
8612
  }
8498
- return `${shortened.trimEnd()}...`;
8613
+ return void 0;
8614
+ }
8615
+ function extractMainHtml(html) {
8616
+ return findElementHtml(html, (tagName) => tagName === "main") ?? findElementHtml(html, (tagName) => tagName === "article") ?? findElementHtml(
8617
+ html,
8618
+ (_tagName, tag) => /\brole\s*=\s*(["'])main\1/i.test(tag)
8619
+ ) ?? html;
8499
8620
  }
8500
- function extractContent(body, contentType, maxChars) {
8621
+ function extractContentDetails(body, contentType, maxChars) {
8501
8622
  const loweredContentType = contentType.toLowerCase();
8502
8623
  const normalizedBody = body.trim();
8503
8624
  if (loweredContentType.includes("html")) {
8504
8625
  try {
8505
- const markdown = htmlToMarkdownConverter.translate(normalizedBody);
8506
- return truncateAtWordBoundary(normalizeWhitespace(markdown), maxChars);
8626
+ const sourceHtml = extractMainHtml(normalizedBody);
8627
+ const markdown = htmlToMarkdownConverter.translate(sourceHtml);
8628
+ const normalizedMarkdown = normalizeWhitespace(markdown);
8629
+ const truncated2 = truncateAtWordBoundary(normalizedMarkdown, maxChars);
8630
+ return {
8631
+ content: truncated2.content,
8632
+ title: extractTitle(normalizedBody),
8633
+ truncated: truncated2.truncated,
8634
+ extractedChars: normalizedMarkdown.length
8635
+ };
8507
8636
  } catch {
8508
8637
  }
8509
8638
  }
8510
8639
  if (loweredContentType.includes("json")) {
8511
8640
  try {
8512
8641
  const parsed = JSON.parse(normalizedBody);
8513
- return truncateAtWordBoundary(JSON.stringify(parsed, null, 2), maxChars);
8642
+ const formatted = JSON.stringify(parsed, null, 2);
8643
+ const truncated2 = truncateAtWordBoundary(formatted, maxChars);
8644
+ return {
8645
+ content: truncated2.content,
8646
+ truncated: truncated2.truncated,
8647
+ extractedChars: formatted.length
8648
+ };
8514
8649
  } catch {
8515
- return truncateAtWordBoundary(
8516
- normalizeWhitespace(normalizedBody),
8517
- maxChars
8518
- );
8650
+ const normalizedText2 = normalizeWhitespace(normalizedBody);
8651
+ const truncated2 = truncateAtWordBoundary(normalizedText2, maxChars);
8652
+ return {
8653
+ content: truncated2.content,
8654
+ truncated: truncated2.truncated,
8655
+ extractedChars: normalizedText2.length
8656
+ };
8519
8657
  }
8520
8658
  }
8521
- return truncateAtWordBoundary(normalizeWhitespace(normalizedBody), maxChars);
8659
+ const normalizedText = normalizeWhitespace(normalizedBody);
8660
+ const truncated = truncateAtWordBoundary(normalizedText, maxChars);
8661
+ return {
8662
+ content: truncated.content,
8663
+ truncated: truncated.truncated,
8664
+ extractedChars: normalizedText.length
8665
+ };
8522
8666
  }
8523
8667
  async function extractWebFetchResponse(url, response, maxChars = DEFAULT_MAX_CHARS) {
8524
8668
  const safeMaxChars = Math.max(500, Math.min(maxChars, MAX_FETCH_CHARS));
@@ -8534,8 +8678,16 @@ async function extractWebFetchResponse(url, response, maxChars = DEFAULT_MAX_CHA
8534
8678
  FETCH_TIMEOUT_MS,
8535
8679
  "read"
8536
8680
  );
8537
- const text = extractContent(body, contentType, safeMaxChars);
8538
- return { url: url.toString(), content: text };
8681
+ const extracted = extractContentDetails(body, contentType, safeMaxChars);
8682
+ return {
8683
+ url: url.toString(),
8684
+ content: extracted.content,
8685
+ ...extracted.title ? { title: extracted.title } : {},
8686
+ content_type: contentType || "unknown",
8687
+ source_bytes: Buffer.byteLength(body, "utf8"),
8688
+ extracted_chars: extracted.extractedChars,
8689
+ truncated: extracted.truncated
8690
+ };
8539
8691
  }
8540
8692
 
8541
8693
  // src/chat/tools/web/fetch-tool.ts
@@ -8558,6 +8710,7 @@ function extractHttpStatusFromMessage(message) {
8558
8710
  return Number.isFinite(parsed) ? parsed : null;
8559
8711
  }
8560
8712
  function createWebFetchTool(hooks) {
8713
+ const override = hooks.toolOverrides?.webFetch;
8561
8714
  return tool({
8562
8715
  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.",
8563
8716
  annotations: {
@@ -8579,6 +8732,9 @@ function createWebFetchTool(hooks) {
8579
8732
  )
8580
8733
  }),
8581
8734
  execute: async ({ url, max_chars }) => {
8735
+ if (override?.execute) {
8736
+ return override.execute({ url, max_chars });
8737
+ }
8582
8738
  try {
8583
8739
  const safeUrl = await assertPublicUrl(url);
8584
8740
  const response = await withTimeout(
@@ -8671,7 +8827,7 @@ function isAuthFailure(message) {
8671
8827
  const normalized = message.toLowerCase();
8672
8828
  return normalized.includes("missing ai gateway credentials") || normalized.includes("authentication failed");
8673
8829
  }
8674
- function createWebSearchTool() {
8830
+ function createWebSearchTool(override) {
8675
8831
  return tool({
8676
8832
  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.",
8677
8833
  annotations: {
@@ -8694,6 +8850,9 @@ function createWebSearchTool() {
8694
8850
  )
8695
8851
  }),
8696
8852
  execute: async ({ query, max_results }) => {
8853
+ if (override?.execute) {
8854
+ return override.execute({ query, max_results });
8855
+ }
8697
8856
  const maxResults = max_results ?? 3;
8698
8857
  const model = process.env.AI_WEB_SEARCH_MODEL ?? DEFAULT_SEARCH_MODEL;
8699
8858
  const controller = new AbortController();
@@ -8827,7 +8986,7 @@ function createTools(availableSkills, hooks = {}, context) {
8827
8986
  findFiles: createFindFilesTool(),
8828
8987
  listDir: createListDirTool(),
8829
8988
  writeFile: createWriteFileTool(),
8830
- webSearch: createWebSearchTool(),
8989
+ webSearch: createWebSearchTool(hooks.toolOverrides?.webSearch),
8831
8990
  webFetch: createWebFetchTool(hooks),
8832
8991
  imageGenerate: createImageGenerateTool(
8833
8992
  hooks,
@@ -8935,14 +9094,11 @@ function buildSandboxEgressNetworkPolicy() {
8935
9094
  }
8936
9095
  return { allow };
8937
9096
  }
8938
- async function resolveSandboxCommandEnvironment(provider) {
9097
+ async function resolveSandboxCommandEnvironment() {
8939
9098
  const env = {};
8940
9099
  for (const plugin of getPluginProviders().sort(
8941
9100
  (left, right) => left.manifest.name.localeCompare(right.manifest.name)
8942
9101
  )) {
8943
- if (provider && plugin.manifest.name !== provider) {
8944
- continue;
8945
- }
8946
9102
  Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
8947
9103
  const credentials = plugin.manifest.credentials;
8948
9104
  if (credentials) {
@@ -9213,6 +9369,14 @@ function isSnapshottingError(error) {
9213
9369
  return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
9214
9370
  });
9215
9371
  }
9372
+ function isSandboxCommandStreamInterruptedError(error) {
9373
+ return findInErrorChain(error, (candidate) => {
9374
+ if (!(candidate instanceof Error)) {
9375
+ return false;
9376
+ }
9377
+ return candidate.name === "StreamError" && candidate.message.toLowerCase().includes("stream ended before command finished");
9378
+ });
9379
+ }
9216
9380
  function wrapSandboxSetupError(error) {
9217
9381
  try {
9218
9382
  const details = getSandboxErrorDetails(error);
@@ -9258,6 +9422,25 @@ import { randomUUID as randomUUID4 } from "crypto";
9258
9422
  import { Sandbox } from "@vercel/sandbox";
9259
9423
  import { createBashTool as createBashTool2 } from "bash-tool";
9260
9424
 
9425
+ // src/chat/sandbox/fault-injection.ts
9426
+ var STREAM_INTERRUPT_FAULT_ENV = "JUNIOR_EVAL_FAULT_SANDBOX_BASH_STREAM_INTERRUPTS";
9427
+ function consumeSandboxBashStreamInterruptFault() {
9428
+ if (process.env.JUNIOR_EVAL_ENABLE_FAULTS !== "1") {
9429
+ return void 0;
9430
+ }
9431
+ const remaining = Number.parseInt(
9432
+ process.env[STREAM_INTERRUPT_FAULT_ENV] ?? "0",
9433
+ 10
9434
+ );
9435
+ if (!Number.isFinite(remaining) || remaining <= 0) {
9436
+ return void 0;
9437
+ }
9438
+ process.env[STREAM_INTERRUPT_FAULT_ENV] = String(remaining - 1);
9439
+ return Object.assign(new Error("Stream ended before command finished"), {
9440
+ name: "StreamError"
9441
+ });
9442
+ }
9443
+
9261
9444
  // src/chat/sandbox/skill-sync.ts
9262
9445
  import fs3 from "fs/promises";
9263
9446
  import path9 from "path";
@@ -9386,6 +9569,62 @@ function outputText(value) {
9386
9569
  fs.writeFileSync(process.stdout.fd, value);
9387
9570
  }
9388
9571
 
9572
+ const repoFiles = {
9573
+ "packages/junior/src/chat/sandbox/egress-policy.ts": \`import { resolveAuthTokenPlaceholder } from "@/chat/plugins/auth/auth-token-placeholder";
9574
+ import { resolvePluginCommandEnv } from "@/chat/plugins/command-env";
9575
+ import { getPluginProviders } from "@/chat/plugins/registry";
9576
+
9577
+ /** Build the policy that forwards provider requests back to Junior for credentials. */
9578
+ export function buildSandboxEgressNetworkPolicy() {
9579
+ // Plugin credential domains are forwarded through the host so the sandbox can
9580
+ // activate requester-bound credentials for the current turn.
9581
+ }
9582
+
9583
+ /** Resolve non-secret command environment values for registered sandbox providers. */
9584
+ export async function resolveSandboxCommandEnvironment() {
9585
+ const env = {};
9586
+ for (const plugin of getPluginProviders()) {
9587
+ Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
9588
+ const credentials = plugin.manifest.credentials;
9589
+ if (credentials) {
9590
+ env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
9591
+ }
9592
+ }
9593
+ return env;
9594
+ }
9595
+ \`,
9596
+ "packages/junior/src/chat/plugins/registry.ts": \`import { createGitHubAppBroker } from "@/chat/plugins/auth/github-app-broker";
9597
+
9598
+ export function createPluginBroker(provider, deps) {
9599
+ const plugin = ensurePluginsLoaded().pluginsByName.get(provider);
9600
+ const { credentials, name } = plugin.manifest;
9601
+ if (credentials.type === "github-app") {
9602
+ return createGitHubAppBroker(plugin.manifest, credentials);
9603
+ }
9604
+ }
9605
+ \`,
9606
+ "packages/junior-github/plugin.yaml": \`name: github
9607
+ description: GitHub issue, pull request, and repository workflows via GitHub App
9608
+
9609
+ credentials:
9610
+ type: github-app
9611
+ domains:
9612
+ - api.github.com
9613
+ - github.com
9614
+ auth-token-env: GITHUB_TOKEN
9615
+ auth-token-placeholder: ghp_host_managed_credential
9616
+ \`,
9617
+ };
9618
+
9619
+ function writeRepoFixture(targetDir) {
9620
+ fs.mkdirSync(targetDir, { recursive: true });
9621
+ for (const [relativePath, content] of Object.entries(repoFiles)) {
9622
+ const filePath = path.join(targetDir, relativePath);
9623
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
9624
+ fs.writeFileSync(filePath, content);
9625
+ }
9626
+ }
9627
+
9389
9628
  function fallbackToRealGh() {
9390
9629
  for (const binary of fallbackBinaries) {
9391
9630
  if (!fs.existsSync(binary)) {
@@ -9433,9 +9672,33 @@ if (args[0] === "repo" && args[1] === "view") {
9433
9672
  process.exit(0);
9434
9673
  }
9435
9674
 
9675
+ if (args[0] === "repo" && args[1] === "clone") {
9676
+ const positionals = getPositionals();
9677
+ const repo = positionals[2] || repoValue();
9678
+ const targetDir = positionals[3] || repo.split("/").pop() || "repo";
9679
+ writeRepoFixture(path.resolve(process.cwd(), targetDir));
9680
+ outputText("Cloning into '" + targetDir + "'...\\n");
9681
+ process.exit(0);
9682
+ }
9683
+
9436
9684
  if (args[0] === "api") {
9437
9685
  const positionals = getPositionals();
9438
9686
  const route = positionals[1] || "";
9687
+ if (route.includes("/git/trees/")) {
9688
+ const paths = Object.keys(repoFiles);
9689
+ const jq = getFlag("--jq");
9690
+ if (jq && jq.includes(".tree[].path")) {
9691
+ outputText(paths.join("\\n") + "\\n");
9692
+ } else {
9693
+ outputJson({
9694
+ tree: paths.map((filePath) => ({
9695
+ path: filePath,
9696
+ type: "blob",
9697
+ })),
9698
+ });
9699
+ }
9700
+ process.exit(0);
9701
+ }
9439
9702
  if (route.includes("/comments")) {
9440
9703
  outputJson([]);
9441
9704
  process.exit(0);
@@ -9887,6 +10150,15 @@ function parseKeepAliveMs() {
9887
10150
  );
9888
10151
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
9889
10152
  }
10153
+ function getCommandStreamInterruptedResult() {
10154
+ return {
10155
+ stdout: "",
10156
+ stderr: "Command stream ended before the command finished. The command may still have produced side effects; inspect the workspace or rerun only if it is safe.",
10157
+ exitCode: 125,
10158
+ stdoutTruncated: false,
10159
+ stderrTruncated: false
10160
+ };
10161
+ }
9890
10162
  function createSandboxSessionManager(options) {
9891
10163
  let sandbox = null;
9892
10164
  let sandboxIdHint = options?.sandboxId;
@@ -10298,6 +10570,10 @@ function createSandboxSessionManager(options) {
10298
10570
  timedOut = true;
10299
10571
  controller.abort();
10300
10572
  }, input.timeoutMs) : void 0;
10573
+ const streamInterruptFault = consumeSandboxBashStreamInterruptFault();
10574
+ if (streamInterruptFault) {
10575
+ throw streamInterruptFault;
10576
+ }
10301
10577
  const commandResult2 = await sandboxInstance.runCommand({
10302
10578
  cmd: "bash",
10303
10579
  args: ["-c", script],
@@ -10317,6 +10593,9 @@ function createSandboxSessionManager(options) {
10317
10593
  timedOut: true
10318
10594
  };
10319
10595
  }
10596
+ if (isSandboxCommandStreamInterruptedError(error)) {
10597
+ return getCommandStreamInterruptedResult();
10598
+ }
10320
10599
  throw error;
10321
10600
  } finally {
10322
10601
  if (timeoutId) {
@@ -10426,10 +10705,7 @@ function createSandboxExecutor(options) {
10426
10705
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
10427
10706
  timeoutMs: options?.timeoutMs,
10428
10707
  traceContext,
10429
- commandEnv: credentialEgress ? async () => {
10430
- const provider = credentialEgress.activeProvider?.();
10431
- return provider ? await resolveSandboxCommandEnvironment(provider) : {};
10432
- } : void 0,
10708
+ commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
10433
10709
  createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
10434
10710
  beforeCommand: authorizeSandboxEgressForCommand,
10435
10711
  afterCommand: clearSandboxEgressForCommand,
@@ -10513,7 +10789,7 @@ function createSandboxExecutor(options) {
10513
10789
  const executeReadFileTool = async (rawInput) => {
10514
10790
  const filePath = String(rawInput.path ?? "").trim();
10515
10791
  if (!filePath) {
10516
- throw new Error("path is required");
10792
+ throw new ToolInputError("path is required");
10517
10793
  }
10518
10794
  const offset = positiveInteger(rawInput.offset);
10519
10795
  const limit = positiveInteger(rawInput.limit);
@@ -10586,7 +10862,7 @@ function createSandboxExecutor(options) {
10586
10862
  const executeWriteFileTool = async (rawInput) => {
10587
10863
  const filePath = String(rawInput.path ?? "").trim();
10588
10864
  if (!filePath) {
10589
- throw new Error("path is required");
10865
+ throw new ToolInputError("path is required");
10590
10866
  }
10591
10867
  const content = String(rawInput.content ?? "");
10592
10868
  logSandboxBootRequest("tool.writeFile", {
@@ -10620,10 +10896,10 @@ function createSandboxExecutor(options) {
10620
10896
  const executeEditFileTool = async (rawInput) => {
10621
10897
  const filePath = String(rawInput.path ?? "").trim();
10622
10898
  if (!filePath) {
10623
- throw new Error("path is required");
10899
+ throw new ToolInputError("path is required");
10624
10900
  }
10625
10901
  if (!Array.isArray(rawInput.edits)) {
10626
- throw new Error("edits is required");
10902
+ throw new ToolInputError("edits is required");
10627
10903
  }
10628
10904
  logSandboxBootRequest("tool.editFile", {
10629
10905
  "file.path": filePath
@@ -10651,7 +10927,7 @@ function createSandboxExecutor(options) {
10651
10927
  const executeGrepTool = async (rawInput) => {
10652
10928
  const pattern = String(rawInput.pattern ?? "");
10653
10929
  if (!pattern) {
10654
- throw new Error("pattern is required");
10930
+ throw new ToolInputError("pattern is required");
10655
10931
  }
10656
10932
  logSandboxBootRequest("tool.grep");
10657
10933
  const contextLines = positiveInteger(rawInput.context);
@@ -10683,7 +10959,7 @@ function createSandboxExecutor(options) {
10683
10959
  const executeFindFilesTool = async (rawInput) => {
10684
10960
  const pattern = String(rawInput.pattern ?? "");
10685
10961
  if (!pattern) {
10686
- throw new Error("pattern is required");
10962
+ throw new ToolInputError("pattern is required");
10687
10963
  }
10688
10964
  logSandboxBootRequest("tool.findFiles");
10689
10965
  const limit = positiveInteger(rawInput.limit);
@@ -10732,7 +11008,7 @@ function createSandboxExecutor(options) {
10732
11008
  const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
10733
11009
  if (params.toolName === "bash") {
10734
11010
  if (!bashCommand) {
10735
- throw new Error("command is required");
11011
+ throw new ToolInputError("command is required");
10736
11012
  }
10737
11013
  if (options?.runBashCustomCommand) {
10738
11014
  const custom = await options.runBashCustomCommand(bashCommand);
@@ -10934,6 +11210,28 @@ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
10934
11210
  function agentTurnSessionKey(conversationId, sessionId) {
10935
11211
  return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
10936
11212
  }
11213
+ function toFiniteNonNegativeNumber(value) {
11214
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
11215
+ }
11216
+ function parseAgentTurnUsage(value) {
11217
+ if (!isRecord(value)) {
11218
+ return void 0;
11219
+ }
11220
+ const usage = {};
11221
+ for (const field of [
11222
+ "inputTokens",
11223
+ "outputTokens",
11224
+ "cachedInputTokens",
11225
+ "cacheCreationTokens",
11226
+ "totalTokens"
11227
+ ]) {
11228
+ const count = toFiniteNonNegativeNumber(value[field]);
11229
+ if (count !== void 0) {
11230
+ usage[field] = count;
11231
+ }
11232
+ }
11233
+ return Object.keys(usage).length > 0 ? usage : void 0;
11234
+ }
10937
11235
  function parseAgentTurnSessionCheckpoint(value) {
10938
11236
  if (typeof value !== "string") {
10939
11237
  return void 0;
@@ -10952,6 +11250,10 @@ function parseAgentTurnSessionCheckpoint(value) {
10952
11250
  const sliceId = parsed.sliceId;
10953
11251
  const checkpointVersion = parsed.checkpointVersion;
10954
11252
  const updatedAtMs = parsed.updatedAtMs;
11253
+ const cumulativeDurationMs = toFiniteNonNegativeNumber(
11254
+ parsed.cumulativeDurationMs
11255
+ );
11256
+ const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
10955
11257
  if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
10956
11258
  return void 0;
10957
11259
  }
@@ -10962,6 +11264,8 @@ function parseAgentTurnSessionCheckpoint(value) {
10962
11264
  sliceId,
10963
11265
  state: status,
10964
11266
  updatedAtMs,
11267
+ ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
11268
+ ...cumulativeUsage ? { cumulativeUsage } : {},
10965
11269
  piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
10966
11270
  ...Array.isArray(parsed.loadedSkillNames) ? {
10967
11271
  loadedSkillNames: parsed.loadedSkillNames.filter(
@@ -10999,6 +11303,13 @@ async function upsertAgentTurnSessionCheckpoint(args) {
10999
11303
  state: args.state,
11000
11304
  updatedAtMs: Date.now(),
11001
11305
  piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
11306
+ ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
11307
+ cumulativeDurationMs: Math.max(
11308
+ 0,
11309
+ Math.floor(args.cumulativeDurationMs)
11310
+ )
11311
+ } : {},
11312
+ ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
11002
11313
  ...Array.isArray(args.loadedSkillNames) ? {
11003
11314
  loadedSkillNames: args.loadedSkillNames.filter(
11004
11315
  (value) => typeof value === "string"
@@ -11030,6 +11341,8 @@ async function supersedeAgentTurnSessionCheckpoint(args) {
11030
11341
  sliceId: existing.sliceId,
11031
11342
  state: "superseded",
11032
11343
  piMessages: existing.piMessages,
11344
+ cumulativeDurationMs: existing.cumulativeDurationMs,
11345
+ cumulativeUsage: existing.cumulativeUsage,
11033
11346
  loadedSkillNames: existing.loadedSkillNames,
11034
11347
  resumeReason: existing.resumeReason,
11035
11348
  resumedFromSliceId: existing.resumedFromSliceId,
@@ -11291,6 +11604,11 @@ function createPluginAuthOrchestration(deps, abortAgent) {
11291
11604
  }
11292
11605
 
11293
11606
  // src/chat/tools/execution/tool-error-handler.ts
11607
+ function getToolErrorType(error) {
11608
+ if (error instanceof McpToolError) return "tool_error";
11609
+ if (error instanceof ToolInputError) return "tool_input_error";
11610
+ return error instanceof Error ? error.name : "tool_execution_error";
11611
+ }
11294
11612
  function getToolErrorAttributes(error) {
11295
11613
  if (!(error instanceof SlackActionError)) {
11296
11614
  return {};
@@ -11304,7 +11622,7 @@ function getToolErrorAttributes(error) {
11304
11622
  };
11305
11623
  }
11306
11624
  function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
11307
- const errorType = getMcpAwareErrorType(error, "tool_execution_error");
11625
+ const errorType = getToolErrorType(error);
11308
11626
  const errorMessage = getMcpAwareErrorMessage(error);
11309
11627
  setSpanAttributes({
11310
11628
  "error.type": errorType,
@@ -11343,7 +11661,8 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
11343
11661
  "Agent tool call failed"
11344
11662
  );
11345
11663
  }
11346
- if (!(error instanceof McpToolError)) {
11664
+ const isExpectedToolFailure = error instanceof McpToolError || error instanceof ToolInputError;
11665
+ if (!isExpectedToolFailure) {
11347
11666
  logException(
11348
11667
  error,
11349
11668
  "agent_tool_call_failed",
@@ -11898,7 +12217,95 @@ function toAgentThinkingLevel(level) {
11898
12217
  }
11899
12218
  }
11900
12219
 
12220
+ // src/chat/usage.ts
12221
+ var COMPONENT_USAGE_FIELDS = [
12222
+ "inputTokens",
12223
+ "outputTokens",
12224
+ "cachedInputTokens",
12225
+ "cacheCreationTokens"
12226
+ ];
12227
+ function hasAgentTurnUsage(usage) {
12228
+ return Boolean(
12229
+ usage && Object.values(usage).some(
12230
+ (value) => typeof value === "number" && Number.isFinite(value)
12231
+ )
12232
+ );
12233
+ }
12234
+ function getFiniteCount(value) {
12235
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
12236
+ }
12237
+ function getComponentTotal(usage) {
12238
+ let total;
12239
+ for (const field of COMPONENT_USAGE_FIELDS) {
12240
+ const value = getFiniteCount(usage[field]);
12241
+ if (value === void 0) continue;
12242
+ total = (total ?? 0) + value;
12243
+ }
12244
+ return total;
12245
+ }
12246
+ function addAgentTurnUsage(...usages) {
12247
+ const components = {};
12248
+ let componentTotal;
12249
+ let totalOnlyTokens;
12250
+ for (const usage of usages) {
12251
+ if (!usage) continue;
12252
+ const usageComponentTotal = getComponentTotal(usage);
12253
+ if (usageComponentTotal !== void 0) {
12254
+ componentTotal = (componentTotal ?? 0) + usageComponentTotal;
12255
+ for (const field of COMPONENT_USAGE_FIELDS) {
12256
+ const value = getFiniteCount(usage[field]);
12257
+ if (value === void 0) continue;
12258
+ components[field] = (components[field] ?? 0) + value;
12259
+ }
12260
+ continue;
12261
+ }
12262
+ const totalTokens = getFiniteCount(usage.totalTokens);
12263
+ if (totalTokens !== void 0) {
12264
+ totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
12265
+ }
12266
+ }
12267
+ if (totalOnlyTokens !== void 0) {
12268
+ return {
12269
+ totalTokens: totalOnlyTokens + (componentTotal ?? 0)
12270
+ };
12271
+ }
12272
+ return hasAgentTurnUsage(components) ? components : void 0;
12273
+ }
12274
+
11901
12275
  // src/chat/services/turn-checkpoint.ts
12276
+ function logCheckpointError(error, eventName, args, attributes, message) {
12277
+ logException(
12278
+ error,
12279
+ eventName,
12280
+ {
12281
+ slackThreadId: args.logContext.threadId,
12282
+ slackUserId: args.logContext.requesterId,
12283
+ slackChannelId: args.logContext.channelId,
12284
+ runId: args.logContext.runId,
12285
+ assistantUserName: args.logContext.assistantUserName,
12286
+ modelId: args.logContext.modelId
12287
+ },
12288
+ {
12289
+ "app.ai.resume_conversation_id": args.conversationId,
12290
+ "app.ai.resume_session_id": args.sessionId,
12291
+ ...attributes
12292
+ },
12293
+ message
12294
+ );
12295
+ }
12296
+ function addDurationMs(prior, current) {
12297
+ const total = [prior, current].reduce((sum, value) => {
12298
+ if (typeof value !== "number" || !Number.isFinite(value)) {
12299
+ return sum;
12300
+ }
12301
+ return (sum ?? 0) + Math.max(0, Math.floor(value));
12302
+ }, void 0);
12303
+ return total;
12304
+ }
12305
+ function isContinuableBoundary(messages) {
12306
+ const lastRole = getPiMessageRole(messages.at(-1));
12307
+ return lastRole === "user" || lastRole === "toolResult";
12308
+ }
11902
12309
  async function loadTurnCheckpoint(ctx) {
11903
12310
  const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
11904
12311
  const existingCheckpoint = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionCheckpoint(ctx.conversationId, ctx.sessionId) : void 0;
@@ -11912,15 +12319,70 @@ async function loadTurnCheckpoint(ctx) {
11912
12319
  existingCheckpoint
11913
12320
  };
11914
12321
  }
12322
+ async function persistRunningCheckpoint(args) {
12323
+ if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
12324
+ return;
12325
+ }
12326
+ try {
12327
+ const latestCheckpoint = await getAgentTurnSessionCheckpoint(
12328
+ args.conversationId,
12329
+ args.sessionId
12330
+ );
12331
+ await upsertAgentTurnSessionCheckpoint({
12332
+ conversationId: args.conversationId,
12333
+ cumulativeDurationMs: latestCheckpoint?.cumulativeDurationMs,
12334
+ cumulativeUsage: latestCheckpoint?.cumulativeUsage,
12335
+ sessionId: args.sessionId,
12336
+ sliceId: args.sliceId,
12337
+ state: "running",
12338
+ piMessages: args.messages,
12339
+ loadedSkillNames: args.loadedSkillNames
12340
+ });
12341
+ } catch (checkpointError) {
12342
+ logCheckpointError(
12343
+ checkpointError,
12344
+ "agent_turn_running_checkpoint_failed",
12345
+ args,
12346
+ {
12347
+ "app.ai.resume_slice_id": args.sliceId
12348
+ },
12349
+ "Failed to persist running turn checkpoint"
12350
+ );
12351
+ }
12352
+ }
11915
12353
  async function persistCompletedCheckpoint(args) {
11916
- await upsertAgentTurnSessionCheckpoint({
11917
- conversationId: args.conversationId,
11918
- sessionId: args.sessionId,
11919
- sliceId: args.sliceId,
11920
- state: "completed",
11921
- piMessages: args.allMessages,
11922
- loadedSkillNames: args.loadedSkillNames
11923
- });
12354
+ try {
12355
+ const latestCheckpoint = await getAgentTurnSessionCheckpoint(
12356
+ args.conversationId,
12357
+ args.sessionId
12358
+ );
12359
+ await upsertAgentTurnSessionCheckpoint({
12360
+ conversationId: args.conversationId,
12361
+ cumulativeDurationMs: addDurationMs(
12362
+ latestCheckpoint?.cumulativeDurationMs,
12363
+ args.currentDurationMs
12364
+ ),
12365
+ cumulativeUsage: addAgentTurnUsage(
12366
+ latestCheckpoint?.cumulativeUsage,
12367
+ args.currentUsage
12368
+ ),
12369
+ sessionId: args.sessionId,
12370
+ sliceId: args.sliceId,
12371
+ state: "completed",
12372
+ piMessages: args.allMessages,
12373
+ loadedSkillNames: args.loadedSkillNames
12374
+ });
12375
+ } catch (checkpointError) {
12376
+ logCheckpointError(
12377
+ checkpointError,
12378
+ "agent_turn_completed_checkpoint_failed",
12379
+ args,
12380
+ {
12381
+ "app.ai.resume_slice_id": args.sliceId
12382
+ },
12383
+ "Failed to persist completed turn checkpoint"
12384
+ );
12385
+ }
11924
12386
  }
11925
12387
  async function persistAuthPauseCheckpoint(args) {
11926
12388
  const nextSliceId = args.currentSliceId + 1;
@@ -11934,6 +12396,14 @@ async function persistAuthPauseCheckpoint(args) {
11934
12396
  );
11935
12397
  await upsertAgentTurnSessionCheckpoint({
11936
12398
  conversationId: args.conversationId,
12399
+ cumulativeDurationMs: addDurationMs(
12400
+ latestCheckpoint?.cumulativeDurationMs,
12401
+ args.currentDurationMs
12402
+ ),
12403
+ cumulativeUsage: addAgentTurnUsage(
12404
+ latestCheckpoint?.cumulativeUsage,
12405
+ args.currentUsage
12406
+ ),
11937
12407
  sessionId: args.sessionId,
11938
12408
  sliceId: nextSliceId,
11939
12409
  state: "awaiting_resume",
@@ -11944,20 +12414,11 @@ async function persistAuthPauseCheckpoint(args) {
11944
12414
  errorMessage: args.errorMessage
11945
12415
  });
11946
12416
  } catch (checkpointError) {
11947
- logException(
12417
+ logCheckpointError(
11948
12418
  checkpointError,
11949
12419
  "agent_turn_auth_resume_checkpoint_failed",
12420
+ args,
11950
12421
  {
11951
- slackThreadId: args.logContext.threadId,
11952
- slackUserId: args.logContext.requesterId,
11953
- slackChannelId: args.logContext.channelId,
11954
- runId: args.logContext.runId,
11955
- assistantUserName: args.logContext.assistantUserName,
11956
- modelId: args.logContext.modelId
11957
- },
11958
- {
11959
- "app.ai.resume_conversation_id": args.conversationId,
11960
- "app.ai.resume_session_id": args.sessionId,
11961
12422
  "app.ai.resume_from_slice_id": args.currentSliceId,
11962
12423
  "app.ai.resume_next_slice_id": nextSliceId
11963
12424
  },
@@ -11978,6 +12439,14 @@ async function persistTimeoutCheckpoint(args) {
11978
12439
  );
11979
12440
  return await upsertAgentTurnSessionCheckpoint({
11980
12441
  conversationId: args.conversationId,
12442
+ cumulativeDurationMs: addDurationMs(
12443
+ latestCheckpoint?.cumulativeDurationMs,
12444
+ args.currentDurationMs
12445
+ ),
12446
+ cumulativeUsage: addAgentTurnUsage(
12447
+ latestCheckpoint?.cumulativeUsage,
12448
+ args.currentUsage
12449
+ ),
11981
12450
  sessionId: args.sessionId,
11982
12451
  sliceId: nextSliceId,
11983
12452
  state: "awaiting_resume",
@@ -11988,20 +12457,11 @@ async function persistTimeoutCheckpoint(args) {
11988
12457
  errorMessage: args.errorMessage
11989
12458
  });
11990
12459
  } catch (checkpointError) {
11991
- logException(
12460
+ logCheckpointError(
11992
12461
  checkpointError,
11993
12462
  "agent_turn_timeout_resume_checkpoint_failed",
12463
+ args,
11994
12464
  {
11995
- slackThreadId: args.logContext.threadId,
11996
- slackUserId: args.logContext.requesterId,
11997
- slackChannelId: args.logContext.channelId,
11998
- runId: args.logContext.runId,
11999
- assistantUserName: args.logContext.assistantUserName,
12000
- modelId: args.logContext.modelId
12001
- },
12002
- {
12003
- "app.ai.resume_conversation_id": args.conversationId,
12004
- "app.ai.resume_session_id": args.sessionId,
12005
12465
  "app.ai.resume_from_slice_id": args.currentSliceId,
12006
12466
  "app.ai.resume_next_slice_id": nextSliceId
12007
12467
  },
@@ -12123,6 +12583,12 @@ function trimRouterAttachmentText(text) {
12123
12583
  }
12124
12584
  return normalized.length <= MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS ? normalized : `${normalized.slice(0, MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS)}...`;
12125
12585
  }
12586
+ function extractSliceUsage(messages, beforeMessageCount) {
12587
+ const usage = extractGenAiUsageSummary(
12588
+ ...messages.slice(beforeMessageCount).filter(isAssistantMessage)
12589
+ );
12590
+ return hasAgentTurnUsage(usage) ? usage : void 0;
12591
+ }
12126
12592
  function supportsRouterTextPreview(mediaType) {
12127
12593
  const baseMediaType = mediaType.split(";", 1)[0]?.trim().toLowerCase();
12128
12594
  if (!baseMediaType) {
@@ -12277,6 +12743,14 @@ async function generateAssistantReply(messageText, context = {}) {
12277
12743
  let timedOut = false;
12278
12744
  let turnUsage;
12279
12745
  let thinkingSelection;
12746
+ const checkpointLogContext = {
12747
+ threadId: context.correlation?.threadId,
12748
+ requesterId: context.correlation?.requesterId,
12749
+ channelId: context.correlation?.channelId,
12750
+ runId: context.correlation?.runId,
12751
+ assistantUserName: botConfig.userName,
12752
+ modelId: botConfig.modelId
12753
+ };
12280
12754
  const getSandboxMetadata = () => sandboxExecutor ? {
12281
12755
  sandboxId: sandboxExecutor.getSandboxId(),
12282
12756
  sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
@@ -12365,8 +12839,7 @@ async function generateAssistantReply(messageText, context = {}) {
12365
12839
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
12366
12840
  traceContext: spanContext,
12367
12841
  credentialEgress: requesterId ? {
12368
- requesterId,
12369
- activeProvider: () => skillSandbox.getActiveSkill()?.pluginProvider
12842
+ requesterId
12370
12843
  } : void 0,
12371
12844
  onSandboxAcquired: async (sandbox2) => {
12372
12845
  lastKnownSandboxId = sandbox2.sandboxId;
@@ -12542,8 +13015,11 @@ async function generateAssistantReply(messageText, context = {}) {
12542
13015
  });
12543
13016
  const toolChannelId = context.toolChannelId ?? context.correlation?.channelId;
12544
13017
  const channelCapabilities = resolveChannelCapabilities(toolChannelId);
13018
+ const loadableSkills = availableSkills.filter(
13019
+ (skill) => skill.disableModelInvocation !== true || skill.name === invokedSkill?.name
13020
+ );
12545
13021
  const tools = createTools(
12546
- availableSkills,
13022
+ loadableSkills,
12547
13023
  {
12548
13024
  getGeneratedFile: (filename) => generatedFiles.find((file) => file.filename === filename),
12549
13025
  onGeneratedArtifactFiles: (files) => {
@@ -12707,7 +13183,23 @@ async function generateAssistantReply(messageText, context = {}) {
12707
13183
  });
12708
13184
  let hasEmittedText = false;
12709
13185
  let needsSeparator = false;
13186
+ const persistSafeBoundary = async (messages) => {
13187
+ if (!checkpointState.canUseTurnSession || !sessionConversationId || !sessionId) {
13188
+ return;
13189
+ }
13190
+ await persistRunningCheckpoint({
13191
+ conversationId: sessionConversationId,
13192
+ sessionId,
13193
+ sliceId: currentSliceId,
13194
+ messages,
13195
+ loadedSkillNames: loadedSkillNamesForResume,
13196
+ logContext: checkpointLogContext
13197
+ });
13198
+ };
12710
13199
  const unsubscribe = agent.subscribe((event) => {
13200
+ if (event.type === "turn_end" && event.toolResults.length > 0) {
13201
+ return persistSafeBoundary([...agent.state.messages]);
13202
+ }
12711
13203
  if (event.type === "message_start") {
12712
13204
  Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
12713
13205
  logWarn(
@@ -12760,11 +13252,18 @@ async function generateAssistantReply(messageText, context = {}) {
12760
13252
  spanContext,
12761
13253
  async () => {
12762
13254
  let promptResult;
12763
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
13255
+ const freshPromptMessage = {
12764
13256
  role: "user",
12765
13257
  content: promptContentParts,
12766
13258
  timestamp: Date.now()
12767
- });
13259
+ };
13260
+ if (!resumedFromCheckpoint) {
13261
+ await persistSafeBoundary([
13262
+ ...agent.state.messages,
13263
+ freshPromptMessage
13264
+ ]);
13265
+ }
13266
+ const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
12768
13267
  let timeoutId;
12769
13268
  const timeoutPromise = new Promise((_, reject) => {
12770
13269
  timeoutId = setTimeout(() => {
@@ -12817,9 +13316,7 @@ async function generateAssistantReply(messageText, context = {}) {
12817
13316
  agent.state,
12818
13317
  ...outputMessages
12819
13318
  );
12820
- turnUsage = Object.values(usageSummary).some(
12821
- (value) => value !== void 0
12822
- ) ? usageSummary : void 0;
13319
+ turnUsage = hasAgentTurnUsage(usageSummary) ? usageSummary : void 0;
12823
13320
  setSpanAttributes({
12824
13321
  ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
12825
13322
  ...extractGenAiUsageAttributes(usageSummary)
@@ -12843,10 +13340,13 @@ async function generateAssistantReply(messageText, context = {}) {
12843
13340
  if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
12844
13341
  await persistCompletedCheckpoint({
12845
13342
  conversationId: sessionConversationId,
13343
+ currentDurationMs: Date.now() - replyStartedAtMs,
13344
+ currentUsage: turnUsage,
12846
13345
  sessionId,
12847
13346
  sliceId: currentSliceId,
12848
13347
  allMessages: agent.state.messages,
12849
- loadedSkillNames: activeSkills.map((skill) => skill.name)
13348
+ loadedSkillNames: activeSkills.map((skill) => skill.name),
13349
+ logContext: checkpointLogContext
12850
13350
  });
12851
13351
  }
12852
13352
  return buildTurnResult({
@@ -12872,21 +13372,17 @@ async function generateAssistantReply(messageText, context = {}) {
12872
13372
  });
12873
13373
  } catch (error) {
12874
13374
  if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
13375
+ turnUsage = turnUsage ?? extractSliceUsage(timeoutResumeMessages, beforeMessageCount);
12875
13376
  const checkpoint = await persistTimeoutCheckpoint({
12876
13377
  conversationId: timeoutResumeConversationId,
12877
13378
  sessionId: timeoutResumeSessionId,
12878
13379
  currentSliceId: timeoutResumeSliceId,
13380
+ currentDurationMs: Date.now() - replyStartedAtMs,
13381
+ currentUsage: turnUsage,
12879
13382
  messages: timeoutResumeMessages,
12880
13383
  loadedSkillNames: loadedSkillNamesForResume,
12881
13384
  errorMessage: error instanceof Error ? error.message : String(error),
12882
- logContext: {
12883
- threadId: context.correlation?.threadId,
12884
- requesterId: context.correlation?.requesterId,
12885
- channelId: context.correlation?.channelId,
12886
- runId: context.correlation?.runId,
12887
- assistantUserName: botConfig.userName,
12888
- modelId: botConfig.modelId
12889
- }
13385
+ logContext: checkpointLogContext
12890
13386
  });
12891
13387
  if (checkpoint) {
12892
13388
  throw new RetryableTurnError(
@@ -12903,28 +13399,21 @@ async function generateAssistantReply(messageText, context = {}) {
12903
13399
  }
12904
13400
  if (error instanceof AuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
12905
13401
  if (!turnUsage && timeoutResumeMessages.length > 0) {
12906
- const fallbackUsage = extractGenAiUsageSummary(
12907
- ...timeoutResumeMessages.slice(beforeMessageCount).filter(isAssistantMessage)
13402
+ turnUsage = extractSliceUsage(
13403
+ timeoutResumeMessages,
13404
+ beforeMessageCount
12908
13405
  );
12909
- turnUsage = Object.values(fallbackUsage).some(
12910
- (value) => value !== void 0
12911
- ) ? fallbackUsage : void 0;
12912
13406
  }
12913
13407
  const nextSliceId = await persistAuthPauseCheckpoint({
12914
13408
  conversationId: timeoutResumeConversationId,
12915
13409
  sessionId: timeoutResumeSessionId,
12916
13410
  currentSliceId: timeoutResumeSliceId,
13411
+ currentDurationMs: Date.now() - replyStartedAtMs,
13412
+ currentUsage: turnUsage,
12917
13413
  messages: timeoutResumeMessages,
12918
13414
  loadedSkillNames: loadedSkillNamesForResume,
12919
13415
  errorMessage: error.message,
12920
- logContext: {
12921
- threadId: context.correlation?.threadId,
12922
- requesterId: context.correlation?.requesterId,
12923
- channelId: context.correlation?.channelId,
12924
- runId: context.correlation?.runId,
12925
- assistantUserName: botConfig.userName,
12926
- modelId: botConfig.modelId
12927
- }
13416
+ logContext: checkpointLogContext
12928
13417
  });
12929
13418
  throw new RetryableTurnError(
12930
13419
  error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
@@ -13074,9 +13563,10 @@ function finalizeFailedTurnReply(args) {
13074
13563
  ),
13075
13564
  capture.eventName
13076
13565
  );
13566
+ const providerPartialText = args.reply.diagnostics.outcome === "provider_error" ? args.reply.text.trim() : "";
13077
13567
  return {
13078
13568
  ...args.reply,
13079
- text: buildTurnFailureResponse(eventId),
13569
+ text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
13080
13570
  deliveryMode: "thread",
13081
13571
  deliveryPlan: {
13082
13572
  mode: "thread",
@@ -13086,12 +13576,6 @@ function finalizeFailedTurnReply(args) {
13086
13576
  };
13087
13577
  }
13088
13578
 
13089
- // src/chat/services/turn-continuation-response.ts
13090
- var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
13091
- function buildTurnContinuationResponse() {
13092
- return TURN_CONTINUATION_RESPONSE;
13093
- }
13094
-
13095
13579
  // src/chat/slack/assistant-thread/status-render.ts
13096
13580
  var DEFAULT_STATUS_CONTEXTS = {
13097
13581
  thinking: "\u2026",
@@ -13451,16 +13935,12 @@ function createSlackWebApiAssistantStatusSession(args) {
13451
13935
  }
13452
13936
 
13453
13937
  // src/chat/slack/footer.ts
13454
- var SENTRY_CONVERSATION_SEARCH_STATS_PERIOD = "14d";
13455
13938
  function escapeSlackMrkdwn(text) {
13456
13939
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13457
13940
  }
13458
13941
  function escapeSlackLinkUrl(url) {
13459
13942
  return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
13460
13943
  }
13461
- function quoteSentrySearchValue(value) {
13462
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
13463
- }
13464
13944
  function getSentryOrgSlug() {
13465
13945
  const slug = process.env.SENTRY_ORG_SLUG?.trim();
13466
13946
  return slug || void 0;
@@ -13476,7 +13956,7 @@ function buildSentryWebBaseUrl(dsn) {
13476
13956
  const path11 = dsn.path ? `/${dsn.path}` : "";
13477
13957
  return `${dsn.protocol}://${dsn.host}${port}${path11}`;
13478
13958
  }
13479
- function getSentryConversationSearchUrl(conversationId) {
13959
+ function getSentryConversationUrl(conversationId) {
13480
13960
  const client2 = sentry_exports.getClient();
13481
13961
  const dsn = client2?.getDsn();
13482
13962
  if (!dsn?.host || !dsn.projectId) {
@@ -13486,18 +13966,14 @@ function getSentryConversationSearchUrl(conversationId) {
13486
13966
  if (!orgSlug) {
13487
13967
  return void 0;
13488
13968
  }
13969
+ const encodedId = encodeURIComponent(conversationId);
13489
13970
  const params = new URLSearchParams();
13490
- params.set(
13491
- "query",
13492
- `gen_ai.conversation.id:${quoteSentrySearchValue(conversationId)}`
13493
- );
13494
13971
  params.set("project", dsn.projectId);
13495
- params.set("statsPeriod", SENTRY_CONVERSATION_SEARCH_STATS_PERIOD);
13496
- const search = `explore/traces/?${params.toString()}`;
13972
+ const path11 = `explore/conversations/${encodedId}/?${params.toString()}`;
13497
13973
  if (isSentrySaasDsnHost(dsn.host)) {
13498
- return `https://${orgSlug}.sentry.io/${search}`;
13974
+ return `https://${orgSlug}.sentry.io/${path11}`;
13499
13975
  }
13500
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${search}`;
13976
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path11}`;
13501
13977
  }
13502
13978
  function formatSlackTokenCount(value) {
13503
13979
  if (value >= 1e6) {
@@ -13552,7 +14028,7 @@ function buildSlackReplyFooter(args) {
13552
14028
  label: "ID",
13553
14029
  value: conversationId
13554
14030
  };
13555
- const conversationUrl = getSentryConversationSearchUrl(conversationId);
14031
+ const conversationUrl = getSentryConversationUrl(conversationId);
13556
14032
  if (conversationUrl) {
13557
14033
  idItem.url = conversationUrl;
13558
14034
  }
@@ -13748,6 +14224,25 @@ async function postSlackApiReplyPosts(args) {
13748
14224
  return lastPostedMessageTs;
13749
14225
  }
13750
14226
 
14227
+ // src/chat/services/turn-continuation-response.ts
14228
+ var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
14229
+ function buildTurnContinuationResponse() {
14230
+ return TURN_CONTINUATION_RESPONSE;
14231
+ }
14232
+
14233
+ // src/chat/slack/turn-continuation-notice.ts
14234
+ function buildSlackTurnContinuationNotice(args) {
14235
+ const text = buildTurnContinuationResponse();
14236
+ const footer = buildSlackReplyFooter({
14237
+ conversationId: args.conversationId
14238
+ });
14239
+ const blocks = footer ? buildSlackReplyBlocks(text, footer) : void 0;
14240
+ return {
14241
+ text,
14242
+ ...blocks ? { blocks } : {}
14243
+ };
14244
+ }
14245
+
13751
14246
  // src/chat/slack/errors.ts
13752
14247
  function getSlackApiErrorCode(error) {
13753
14248
  if (!error || typeof error !== "object") {
@@ -14079,11 +14574,14 @@ async function postResumeFailureReply(args) {
14079
14574
  }
14080
14575
  }
14081
14576
  async function postTurnContinuationNoticeBestEffort(args) {
14577
+ const notice = buildSlackTurnContinuationNotice({
14578
+ conversationId: args.resumeArgs.replyContext?.correlation?.conversationId ?? args.lockKey
14579
+ });
14082
14580
  try {
14083
14581
  await postSlackMessage({
14084
14582
  channelId: args.resumeArgs.channelId,
14085
14583
  threadTs: args.resumeArgs.threadTs,
14086
- text: buildTurnContinuationResponse()
14584
+ ...notice
14087
14585
  });
14088
14586
  } catch (error) {
14089
14587
  logException(
@@ -14187,6 +14685,10 @@ async function resumeSlackTurn(args) {
14187
14685
  status.start();
14188
14686
  const generateReply = args.generateReply ?? generateAssistantReply;
14189
14687
  const replyContext = createResumeReplyContext(args, status);
14688
+ const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
14689
+ replyContext.correlation.conversationId,
14690
+ replyContext.correlation.turnId
14691
+ ) : void 0;
14190
14692
  const replyPromise = generateReply(args.messageText, replyContext);
14191
14693
  const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
14192
14694
  let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
@@ -14210,9 +14712,12 @@ async function resumeSlackTurn(args) {
14210
14712
  await status.stop();
14211
14713
  const footer = buildSlackReplyFooter({
14212
14714
  conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
14213
- durationMs: reply.diagnostics.durationMs,
14715
+ durationMs: typeof priorCheckpoint?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorCheckpoint?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
14214
14716
  thinkingLevel: reply.diagnostics.thinkingLevel,
14215
- usage: reply.diagnostics.usage
14717
+ usage: addAgentTurnUsage(
14718
+ priorCheckpoint?.cumulativeUsage,
14719
+ reply.diagnostics.usage
14720
+ ) ?? reply.diagnostics.usage
14216
14721
  });
14217
14722
  await postSlackApiReplyPosts({
14218
14723
  channelId: args.channelId,
@@ -15063,6 +15568,8 @@ async function resumeCheckpointedOAuthTurn(stored) {
15063
15568
  fullName: userMessage.author.fullName
15064
15569
  },
15065
15570
  correlation: {
15571
+ conversationId: stored.resumeConversationId,
15572
+ turnId: resolvedSessionId,
15066
15573
  channelId: stored.channelId,
15067
15574
  threadTs: stored.threadTs,
15068
15575
  requesterId: userMessage.author.userId
@@ -17563,7 +18070,51 @@ function maybeUpdateAssistantTitle(args) {
17563
18070
  })();
17564
18071
  }
17565
18072
 
18073
+ // src/chat/services/provider-default-config.ts
18074
+ var GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
18075
+ var GITHUB_REPO_RE = new RegExp(
18076
+ String.raw`^\s*(?:set|use)\s+(?:the\s+)?default\s+(?:github\s+)?repo(?:sitory)?\s+(?:to|as)\s+(${GITHUB_REPO_PART}/${GITHUB_REPO_PART})(?:\s+for\s+this\s+channel)?[.!?]?\s*$`,
18077
+ "i"
18078
+ );
18079
+ async function maybeApplyProviderDefaultConfigRequest(args) {
18080
+ const match = GITHUB_REPO_RE.exec(args.text);
18081
+ const repo = match?.[1];
18082
+ if (!repo || !args.channelConfiguration) {
18083
+ return null;
18084
+ }
18085
+ await args.channelConfiguration.set({
18086
+ key: "github.repo",
18087
+ value: repo,
18088
+ updatedBy: args.requesterId,
18089
+ source: "provider-default-config"
18090
+ });
18091
+ return {
18092
+ text: `Default GitHub repo set to \`${repo}\`.`
18093
+ };
18094
+ }
18095
+
17566
18096
  // src/chat/runtime/reply-executor.ts
18097
+ function collectCanvasUrls(artifacts) {
18098
+ return new Set(
18099
+ [
18100
+ artifacts.lastCanvasUrl,
18101
+ ...artifacts.recentCanvases?.map((canvas) => canvas.url) ?? []
18102
+ ].filter((url) => typeof url === "string" && url !== "")
18103
+ );
18104
+ }
18105
+ function getCurrentTurnCanvasUrl(args) {
18106
+ const previousUrls = collectCanvasUrls(args.before);
18107
+ const latestUrls = collectCanvasUrls(args.after);
18108
+ for (const url of latestUrls) {
18109
+ if (!previousUrls.has(url)) {
18110
+ return url;
18111
+ }
18112
+ }
18113
+ return void 0;
18114
+ }
18115
+ function buildCanvasRecoveryReply(canvasUrl) {
18116
+ return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
18117
+ }
17567
18118
  function createReplyToThread(deps) {
17568
18119
  return async function replyToThread(thread, message, options = {}) {
17569
18120
  if (message.author.isMe) {
@@ -17632,9 +18183,17 @@ function createReplyToThread(deps) {
17632
18183
  const postTurnContinuationNotice = async () => {
17633
18184
  try {
17634
18185
  await beforeFirstResponsePost();
17635
- await thread.post(
17636
- buildSlackOutputMessage(buildTurnContinuationResponse())
17637
- );
18186
+ const notice = buildSlackTurnContinuationNotice({ conversationId });
18187
+ const shouldUseSlackFooter = Boolean(notice.blocks?.length) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
18188
+ if (shouldUseSlackFooter && channelId && threadTs) {
18189
+ await postSlackMessage({
18190
+ channelId,
18191
+ threadTs,
18192
+ ...notice
18193
+ });
18194
+ return;
18195
+ }
18196
+ await thread.post(buildSlackOutputMessage(notice.text));
17638
18197
  } catch (error) {
17639
18198
  logException(
17640
18199
  error,
@@ -17708,6 +18267,40 @@ function createReplyToThread(deps) {
17708
18267
  return;
17709
18268
  }
17710
18269
  }
18270
+ const configReply = await maybeApplyProviderDefaultConfigRequest({
18271
+ channelConfiguration: preparedState.channelConfiguration,
18272
+ requesterId: message.author.userId,
18273
+ text: userText
18274
+ });
18275
+ if (configReply) {
18276
+ await beforeFirstResponsePost();
18277
+ await thread.post(buildSlackOutputMessage(configReply.text));
18278
+ markConversationMessage(
18279
+ preparedState.conversation,
18280
+ preparedState.userMessageId,
18281
+ {
18282
+ replied: true,
18283
+ skippedReason: void 0
18284
+ }
18285
+ );
18286
+ upsertConversationMessage(preparedState.conversation, {
18287
+ id: generateConversationId("assistant"),
18288
+ role: "assistant",
18289
+ text: normalizeConversationText(configReply.text),
18290
+ createdAtMs: Date.now(),
18291
+ author: {
18292
+ userName: botConfig.userName,
18293
+ isBot: true
18294
+ },
18295
+ meta: {
18296
+ replied: true
18297
+ }
18298
+ });
18299
+ await persistThreadState(thread, {
18300
+ conversation: preparedState.conversation
18301
+ });
18302
+ return;
18303
+ }
17711
18304
  startActiveTurn({
17712
18305
  conversation: preparedState.conversation,
17713
18306
  nextTurnId: turnId,
@@ -17789,6 +18382,7 @@ function createReplyToThread(deps) {
17789
18382
  });
17790
18383
  let persistedAtLeastOnce = false;
17791
18384
  let shouldPersistFailureState = true;
18385
+ let latestArtifacts = preparedState.artifacts;
17792
18386
  try {
17793
18387
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
17794
18388
  let reply = await deps.services.generateAssistantReply(userText, {
@@ -17828,6 +18422,7 @@ function createReplyToThread(deps) {
17828
18422
  });
17829
18423
  },
17830
18424
  onArtifactStateUpdated: async (artifacts) => {
18425
+ latestArtifacts = artifacts;
17831
18426
  await persistThreadState(thread, { artifacts });
17832
18427
  },
17833
18428
  onAuthPending: async (pendingAuth) => {
@@ -18045,6 +18640,60 @@ function createReplyToThread(deps) {
18045
18640
  }
18046
18641
  }
18047
18642
  shouldPersistFailureState = true;
18643
+ const createdCanvasUrl = getCurrentTurnCanvasUrl({
18644
+ before: preparedState.artifacts,
18645
+ after: latestArtifacts
18646
+ });
18647
+ if (createdCanvasUrl) {
18648
+ logException(
18649
+ error,
18650
+ "agent_turn_failed_after_canvas_created",
18651
+ turnTraceContext,
18652
+ {
18653
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
18654
+ "app.slack.canvas.has_url": true
18655
+ },
18656
+ "Agent turn failed after creating a Slack canvas"
18657
+ );
18658
+ const recoveryText = buildCanvasRecoveryReply(createdCanvasUrl);
18659
+ await postThreadReply(
18660
+ buildSlackOutputMessage(recoveryText),
18661
+ "thread_reply"
18662
+ );
18663
+ markConversationMessage(
18664
+ preparedState.conversation,
18665
+ preparedState.userMessageId,
18666
+ {
18667
+ replied: true,
18668
+ skippedReason: void 0
18669
+ }
18670
+ );
18671
+ upsertConversationMessage(preparedState.conversation, {
18672
+ id: generateConversationId("assistant"),
18673
+ role: "assistant",
18674
+ text: normalizeConversationText(recoveryText),
18675
+ createdAtMs: Date.now(),
18676
+ author: {
18677
+ userName: botConfig.userName,
18678
+ isBot: true
18679
+ },
18680
+ meta: {
18681
+ replied: true
18682
+ }
18683
+ });
18684
+ markTurnCompleted({
18685
+ conversation: preparedState.conversation,
18686
+ nowMs: Date.now(),
18687
+ updateConversationStats
18688
+ });
18689
+ await persistThreadState(thread, {
18690
+ artifacts: latestArtifacts,
18691
+ conversation: preparedState.conversation
18692
+ });
18693
+ persistedAtLeastOnce = true;
18694
+ shouldPersistFailureState = false;
18695
+ return;
18696
+ }
18048
18697
  throw error;
18049
18698
  } finally {
18050
18699
  if (!persistedAtLeastOnce && shouldPersistFailureState) {
@@ -19185,15 +19834,15 @@ async function defaultWaitUntil() {
19185
19834
  };
19186
19835
  }
19187
19836
  }
19188
- async function resolveBuildPluginPackages() {
19837
+ async function resolveBuildPluginConfig() {
19189
19838
  try {
19190
19839
  const mod = await import("#junior/config");
19191
- return mod.pluginPackages;
19840
+ return mod.plugins;
19192
19841
  } catch {
19193
19842
  const env = process.env.JUNIOR_PLUGIN_PACKAGES;
19194
19843
  if (env) {
19195
19844
  try {
19196
- return JSON.parse(env);
19845
+ return { packages: JSON.parse(env) };
19197
19846
  } catch {
19198
19847
  }
19199
19848
  }
@@ -19201,9 +19850,9 @@ async function resolveBuildPluginPackages() {
19201
19850
  }
19202
19851
  }
19203
19852
  async function createApp(options) {
19204
- setPluginPackages(
19205
- options?.pluginPackages ?? await resolveBuildPluginPackages()
19206
- );
19853
+ const pluginConfig = options?.plugins ?? await resolveBuildPluginConfig();
19854
+ setPluginPackages(pluginConfig?.packages);
19855
+ setPluginConfig(pluginConfig);
19207
19856
  setConfigDefaults(options?.configDefaults);
19208
19857
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
19209
19858
  const app = new Hono();