@sentry/junior 0.61.0 → 0.63.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
@@ -1,18 +1,28 @@
1
1
  import {
2
2
  GET,
3
+ JUNIOR_PERSONALITY,
4
+ TURN_CONTEXT_TAG,
3
5
  abandonAgentTurnSessionRecord,
4
6
  buildSentryConversationUrl,
7
+ buildSlackOutputMessage,
8
+ buildSystemPrompt,
9
+ buildTurnContextPrompt,
5
10
  commitMessages,
11
+ escapeXml,
6
12
  failAgentTurnSessionRecord,
7
13
  getAgentTurnSessionRecord,
14
+ getInterruptionMarker,
8
15
  loadConnectedMcpProviders,
9
16
  loadProjection,
17
+ normalizeSlackStatusText,
10
18
  recordAgentTurnSessionSummary,
11
19
  recordAuthorizationCompleted,
12
20
  recordAuthorizationRequested,
13
21
  recordMcpProviderConnected,
22
+ splitSlackReplyText,
23
+ truncateStatusText,
14
24
  upsertAgentTurnSessionRecord
15
- } from "./chunk-I4FDGMFI.js";
25
+ } from "./chunk-ITZ2F7UE.js";
16
26
  import {
17
27
  discoverSkills,
18
28
  findSkillByName,
@@ -20,25 +30,23 @@ import {
20
30
  parseSkillInvocation
21
31
  } from "./chunk-ITOW4DED.js";
22
32
  import {
23
- SANDBOX_DATA_ROOT,
24
- SANDBOX_SKILLS_ROOT,
25
- SANDBOX_WORKSPACE_ROOT,
26
33
  buildNonInteractiveShellScript,
27
34
  createSandboxInstance,
28
35
  getRuntimeDependencyProfileHash,
29
36
  getVercelSandboxCredentials,
30
37
  isSnapshotMissingError,
31
38
  resolveRuntimeDependencySnapshot,
32
- runNonInteractiveCommand,
33
- sandboxSkillDir,
34
- sandboxSkillFile
35
- } from "./chunk-QDGD5WVN.js";
39
+ runNonInteractiveCommand
40
+ } from "./chunk-LRVKJAR2.js";
36
41
  import {
37
42
  ACTIVE_LOCK_TTL_MS,
38
43
  GEN_AI_PROVIDER_NAME,
39
44
  GEN_AI_SERVER_ADDRESS,
40
45
  GEN_AI_SERVER_PORT,
41
46
  MISSING_GATEWAY_CREDENTIALS_ERROR,
47
+ SANDBOX_DATA_ROOT,
48
+ SANDBOX_SKILLS_ROOT,
49
+ SANDBOX_WORKSPACE_ROOT,
42
50
  botConfig,
43
51
  completeObject,
44
52
  completeText,
@@ -55,12 +63,14 @@ import {
55
63
  resolveGatewayModel,
56
64
  resolveSlackChannelIdFromMessage,
57
65
  resolveSlackChannelIdFromThreadId,
66
+ sandboxSkillDir,
67
+ sandboxSkillFile,
58
68
  toGenAiMessageMetadata,
59
69
  toGenAiMessagesTraceAttributes,
60
70
  toGenAiPayloadMetadata,
61
71
  toGenAiPayloadTraceAttributes,
62
72
  toGenAiTextMetadata
63
- } from "./chunk-FKEKRBUB.js";
73
+ } from "./chunk-PEF6UXTY.js";
64
74
  import {
65
75
  CredentialUnavailableError,
66
76
  buildOAuthTokenRequest,
@@ -105,9 +115,7 @@ import {
105
115
  } from "./chunk-Z3YD6NHK.js";
106
116
  import {
107
117
  homeDir,
108
- listReferenceFiles,
109
- soulPathCandidates,
110
- worldPathCandidates
118
+ listReferenceFiles
111
119
  } from "./chunk-5LUISFEY.js";
112
120
  import "./chunk-2KG3PWR4.js";
113
121
 
@@ -279,6 +287,16 @@ var AgentPluginHookDeniedError = class extends Error {
279
287
  var agentPlugins = [];
280
288
  var AGENT_PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
281
289
  var AGENT_PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
290
+ var AGENT_PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
291
+ "GET",
292
+ "POST",
293
+ "PUT",
294
+ "PATCH",
295
+ "DELETE",
296
+ "HEAD",
297
+ "OPTIONS",
298
+ "ALL"
299
+ ]);
282
300
  function validateLegacyStatePrefixes(plugin) {
283
301
  const prefixes = plugin.pluginConfig?.legacyStatePrefixes;
284
302
  if (prefixes === void 0) {
@@ -368,6 +386,128 @@ function getAgentPluginTools(context) {
368
386
  }
369
387
  return tools;
370
388
  }
389
+ function routeMethods(route, pluginName) {
390
+ const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
391
+ if (methods.length === 0) {
392
+ throw new Error(
393
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
394
+ );
395
+ }
396
+ for (const method of methods) {
397
+ if (!AGENT_PLUGIN_ROUTE_METHODS.has(method)) {
398
+ throw new Error(
399
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
400
+ );
401
+ }
402
+ }
403
+ if (methods.includes("ALL") && methods.length > 1) {
404
+ throw new Error(
405
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
406
+ );
407
+ }
408
+ return methods;
409
+ }
410
+ function getAgentPluginRoutes() {
411
+ const routes = [];
412
+ const seen = /* @__PURE__ */ new Set();
413
+ const methodsByPath = /* @__PURE__ */ new Map();
414
+ for (const plugin of getAgentPlugins()) {
415
+ const hook = plugin.hooks?.routes;
416
+ if (!hook) {
417
+ continue;
418
+ }
419
+ const log = createAgentPluginLogger(plugin.name);
420
+ const pluginRoutes = hook({
421
+ plugin: { name: plugin.name },
422
+ log
423
+ });
424
+ if (!Array.isArray(pluginRoutes)) {
425
+ throw new Error(
426
+ `Trusted plugin routes hook from plugin "${plugin.name}" must return an array`
427
+ );
428
+ }
429
+ for (const route of pluginRoutes) {
430
+ if (!isRecord2(route)) {
431
+ throw new Error(
432
+ `Trusted plugin route from plugin "${plugin.name}" must be an object`
433
+ );
434
+ }
435
+ if (typeof route.path !== "string" || !route.path.startsWith("/")) {
436
+ throw new Error(
437
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must start with /`
438
+ );
439
+ }
440
+ if (typeof route.handler !== "function") {
441
+ throw new Error(
442
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must provide a handler`
443
+ );
444
+ }
445
+ const methods = routeMethods(route, plugin.name);
446
+ const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
447
+ if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
448
+ throw new Error(
449
+ `Trusted plugin route "${route.path}" conflicts with an ALL route for the same path`
450
+ );
451
+ }
452
+ for (const method of methods) {
453
+ const key = `${method}:${route.path}`;
454
+ if (seen.has(key)) {
455
+ throw new Error(
456
+ `Duplicate trusted plugin route "${method} ${route.path}"`
457
+ );
458
+ }
459
+ seen.add(key);
460
+ pathMethods.add(method);
461
+ }
462
+ methodsByPath.set(route.path, pathMethods);
463
+ routes.push({
464
+ ...route,
465
+ pluginName: plugin.name
466
+ });
467
+ }
468
+ }
469
+ return routes;
470
+ }
471
+ function trustedSlackConversationUrl(pluginName, link) {
472
+ const url = typeof link?.url === "string" ? link.url.trim() : "";
473
+ if (!url) {
474
+ return void 0;
475
+ }
476
+ let parsed;
477
+ try {
478
+ parsed = new URL(url);
479
+ } catch (error) {
480
+ throw new Error(
481
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
482
+ { cause: error }
483
+ );
484
+ }
485
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
486
+ throw new Error(
487
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
488
+ );
489
+ }
490
+ return parsed.toString();
491
+ }
492
+ function getAgentPluginSlackConversationLink(conversationId) {
493
+ for (const plugin of getAgentPlugins()) {
494
+ const hook = plugin.hooks?.slackConversationLink;
495
+ if (!hook) {
496
+ continue;
497
+ }
498
+ const log = createAgentPluginLogger(plugin.name);
499
+ const link = hook({
500
+ plugin: { name: plugin.name },
501
+ log,
502
+ conversationId
503
+ });
504
+ const url = trustedSlackConversationUrl(plugin.name, link);
505
+ if (url) {
506
+ return { url };
507
+ }
508
+ }
509
+ return void 0;
510
+ }
371
511
  function isRecord2(value) {
372
512
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
373
513
  }
@@ -500,747 +640,6 @@ function createAgentPluginHookRunner(input = {}) {
500
640
  import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
501
641
  import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
502
642
 
503
- // src/chat/prompt.ts
504
- import fs from "fs";
505
- import path from "path";
506
-
507
- // src/chat/turn-context-tag.ts
508
- var TURN_CONTEXT_TAG = "runtime-turn-context";
509
-
510
- // src/chat/interruption-marker.ts
511
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
512
- function getInterruptionMarker() {
513
- return INTERRUPTED_MARKER;
514
- }
515
-
516
- // src/chat/slack/status-format.ts
517
- var SLACK_STATUS_MAX_LENGTH = 50;
518
- function truncateStatusText(text) {
519
- const trimmed = text.trim();
520
- if (!trimmed) {
521
- return "";
522
- }
523
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
524
- return trimmed;
525
- }
526
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
527
- }
528
-
529
- // src/chat/slack/mrkdwn.ts
530
- function ensureBlockSpacing(text) {
531
- const codeBlockPattern = /^```/;
532
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
533
- const lines = text.split("\n");
534
- const result = [];
535
- let inCodeBlock = false;
536
- for (let i = 0; i < lines.length; i++) {
537
- const line = lines[i];
538
- const isCodeFence = codeBlockPattern.test(line.trimStart());
539
- if (isCodeFence) {
540
- if (!inCodeBlock) {
541
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
542
- if (prev2 !== void 0 && prev2.trim() !== "") {
543
- result.push("");
544
- }
545
- }
546
- inCodeBlock = !inCodeBlock;
547
- result.push(line);
548
- continue;
549
- }
550
- if (inCodeBlock) {
551
- result.push(line);
552
- continue;
553
- }
554
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
555
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
556
- result.push("");
557
- }
558
- result.push(line);
559
- }
560
- return result.join("\n");
561
- }
562
- function renderSlackMrkdwn(text) {
563
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
564
- normalized = ensureBlockSpacing(normalized);
565
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
566
- }
567
- function normalizeSlackStatusText(text) {
568
- const trimmed = text.trim();
569
- if (!trimmed) {
570
- return "";
571
- }
572
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
573
- }
574
-
575
- // src/chat/slack/output.ts
576
- var MAX_INLINE_CHARS = 2200;
577
- var MAX_INLINE_LINES = 45;
578
- var CONTINUED_MARKER = "\n\n[Continued below]";
579
- function countSlackLines(text) {
580
- if (!text) {
581
- return 0;
582
- }
583
- return text.split("\n").length;
584
- }
585
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
586
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
587
- }
588
- function findSplitIndex(text, maxChars) {
589
- if (text.length <= maxChars) {
590
- return text.length;
591
- }
592
- const bounded = text.slice(0, maxChars);
593
- const newlineIndex = bounded.lastIndexOf("\n");
594
- if (newlineIndex > 0) {
595
- return newlineIndex;
596
- }
597
- const spaceIndex = bounded.lastIndexOf(" ");
598
- if (spaceIndex > 0) {
599
- return spaceIndex;
600
- }
601
- return maxChars;
602
- }
603
- function splitByLineBudget(text, maxLines) {
604
- if (maxLines <= 0) {
605
- return "";
606
- }
607
- const lines = text.split("\n");
608
- if (lines.length <= maxLines) {
609
- return text;
610
- }
611
- return lines.slice(0, maxLines).join("\n");
612
- }
613
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
614
- return {
615
- maxChars: Math.max(1, maxChars - suffix.length),
616
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
617
- };
618
- }
619
- function forceSplitBudget(text, budget) {
620
- const lineCount = countSlackLines(text);
621
- return {
622
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
623
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
624
- };
625
- }
626
- function getFenceContinuation(text) {
627
- let open;
628
- for (const line of text.split("\n")) {
629
- const trimmed = line.trimStart();
630
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
631
- if (!openerMatch) {
632
- continue;
633
- }
634
- if (!open) {
635
- open = {
636
- fence: openerMatch[1],
637
- openerLine: trimmed
638
- };
639
- continue;
640
- }
641
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
642
- open = void 0;
643
- }
644
- }
645
- if (!open) {
646
- return null;
647
- }
648
- return {
649
- closeSuffix: text.endsWith("\n") ? open.fence : `
650
- ${open.fence}`,
651
- reopenPrefix: `${open.openerLine}
652
- `
653
- };
654
- }
655
- function appendSlackSuffix(text, marker) {
656
- const carryover = getFenceContinuation(text);
657
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
658
- }
659
- function stripTrailingContinuationMarker(text) {
660
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
661
- }
662
- function takeSlackContinuationChunk(text, budget) {
663
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
664
- if (!rest) {
665
- ({ prefix, rest } = takeSlackInlinePrefix(
666
- text,
667
- forceSplitBudget(text, budget)
668
- ));
669
- }
670
- let carryover = rest ? getFenceContinuation(prefix) : null;
671
- if (!carryover) {
672
- return { prefix, rest };
673
- }
674
- const carryoverBudget = reserveInlineBudgetForSuffix(
675
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
676
- );
677
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
678
- if (!rest) {
679
- ({ prefix, rest } = takeSlackInlinePrefix(
680
- text,
681
- forceSplitBudget(text, carryoverBudget)
682
- ));
683
- }
684
- carryover = rest ? getFenceContinuation(prefix) : null;
685
- if (!carryover) {
686
- return { prefix, rest };
687
- }
688
- return {
689
- prefix,
690
- rest: `${carryover.reopenPrefix}${rest}`
691
- };
692
- }
693
- function takeSlackContinuationPrefix(text, options) {
694
- const budget = {
695
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
696
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
697
- };
698
- const { prefix, rest } = (() => {
699
- if (options?.forceSplit) {
700
- return takeSlackContinuationChunk(text, budget);
701
- }
702
- const initial = takeSlackInlinePrefix(text, budget);
703
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
704
- })();
705
- return {
706
- prefix,
707
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
708
- rest
709
- };
710
- }
711
- function takeSlackInlinePrefix(text, options) {
712
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
713
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
714
- const normalized = text.replace(/\r\n?/g, "\n");
715
- if (!normalized) {
716
- return { prefix: "", rest: "" };
717
- }
718
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
719
- return { prefix: normalized, rest: "" };
720
- }
721
- const lineBounded = splitByLineBudget(normalized, maxLines);
722
- const cutIndex = findSplitIndex(lineBounded, maxChars);
723
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
724
- if (prefix) {
725
- return {
726
- prefix,
727
- rest: normalized.slice(prefix.length).trimStart()
728
- };
729
- }
730
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
731
- return {
732
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
733
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
734
- };
735
- }
736
- function splitSlackReplyText(text, options) {
737
- const normalized = renderSlackMrkdwn(text);
738
- if (!normalized) {
739
- return [];
740
- }
741
- const chunks = [];
742
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
743
- let remaining = normalized;
744
- while (remaining) {
745
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
746
- if (fitsFinalChunk) {
747
- chunks.push(
748
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
749
- );
750
- break;
751
- }
752
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
753
- ...continuationBudget,
754
- forceSplit: true
755
- });
756
- chunks.push(renderedPrefix);
757
- remaining = rest;
758
- }
759
- if (chunks.length === 2) {
760
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
761
- }
762
- return chunks;
763
- }
764
- function getSlackContinuationBudget() {
765
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
766
- }
767
- function buildSlackOutputMessage(text, files) {
768
- const normalized = renderSlackMrkdwn(text);
769
- const fileCount = files?.length ?? 0;
770
- if (!normalized) {
771
- if (fileCount > 0) {
772
- return {
773
- raw: "",
774
- files
775
- };
776
- }
777
- throw new Error(
778
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
779
- );
780
- }
781
- return {
782
- markdown: normalized,
783
- files
784
- };
785
- }
786
- var slackOutputPolicy = {
787
- maxInlineChars: MAX_INLINE_CHARS,
788
- maxInlineLines: MAX_INLINE_LINES
789
- };
790
-
791
- // src/chat/xml.ts
792
- function escapeXml(value) {
793
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
794
- }
795
-
796
- // src/chat/prompt.ts
797
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
798
- function getLoggedMarkdownFiles() {
799
- const globalState = globalThis;
800
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
801
- return globalState.__juniorLoggedMarkdownFiles;
802
- }
803
- function loadOptionalMarkdownFile(candidates, fileName) {
804
- for (const resolved of candidates) {
805
- try {
806
- const raw = fs.readFileSync(resolved, "utf8").trim();
807
- if (raw.length > 0) {
808
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
809
- const logKey = `${fileName}:${resolved}`;
810
- if (!loggedMarkdownFiles.has(logKey)) {
811
- loggedMarkdownFiles.add(logKey);
812
- logInfo(
813
- `${fileName.toLowerCase()}_loaded`,
814
- {},
815
- {
816
- "file.path": resolved
817
- },
818
- `Loaded ${fileName}`
819
- );
820
- }
821
- return raw;
822
- }
823
- } catch {
824
- continue;
825
- }
826
- }
827
- return null;
828
- }
829
- function loadSoul() {
830
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
831
- if (soul) {
832
- return soul;
833
- }
834
- logWarn(
835
- "soul_load_fallback",
836
- {},
837
- {
838
- "app.file.candidates": soulPathCandidates()
839
- },
840
- "SOUL.md not found; using built-in default personality"
841
- );
842
- return DEFAULT_SOUL;
843
- }
844
- function loadWorld() {
845
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
846
- }
847
- var JUNIOR_PERSONALITY = (() => {
848
- try {
849
- return loadSoul();
850
- } catch (error) {
851
- logWarn(
852
- "soul_load_failed",
853
- {},
854
- {
855
- "exception.message": error instanceof Error ? error.message : String(error)
856
- },
857
- "Failed to load SOUL.md; using built-in default personality"
858
- );
859
- return DEFAULT_SOUL;
860
- }
861
- })();
862
- var JUNIOR_WORLD = (() => {
863
- try {
864
- return loadWorld();
865
- } catch (error) {
866
- logWarn(
867
- "world_load_failed",
868
- {},
869
- {
870
- "exception.message": error instanceof Error ? error.message : String(error)
871
- },
872
- "Failed to load WORLD.md; omitting world prompt context"
873
- );
874
- return null;
875
- }
876
- })();
877
- function workspaceSkillDir(skillName) {
878
- return sandboxSkillDir(skillName);
879
- }
880
- function formatConfigurationValue(value) {
881
- if (typeof value === "string") {
882
- return escapeXml(value);
883
- }
884
- try {
885
- return escapeXml(JSON.stringify(value));
886
- } catch {
887
- return escapeXml(String(value));
888
- }
889
- }
890
- function renderRequesterBlock(fields) {
891
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key, value]) => `- ${key}: ${escapeXml(value)}`);
892
- if (lines.length === 0) {
893
- return null;
894
- }
895
- return ["<requester>", ...lines, "</requester>"];
896
- }
897
- function renderTag(tag, lines) {
898
- return [`<${tag}>`, ...lines, `</${tag}>`];
899
- }
900
- function renderTagBlock(tag, content) {
901
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
902
- }
903
- function formatSkillEntry(skill) {
904
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
905
- const lines = [];
906
- lines.push(" <skill>");
907
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
908
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
909
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
910
- lines.push(" </skill>");
911
- return lines;
912
- }
913
- function formatAvailableSkillsForPrompt(skills, invocation) {
914
- const autoSelectable = skills.filter(
915
- (s) => s.disableModelInvocation !== true
916
- );
917
- const invokedExplicitOnly = invocation ? skills.filter(
918
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
919
- ) : [];
920
- const sections = [];
921
- if (autoSelectable.length > 0) {
922
- const available = [
923
- "<available-skills>",
924
- "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
925
- ];
926
- for (const skill of autoSelectable) {
927
- available.push(...formatSkillEntry(skill));
928
- }
929
- available.push("</available-skills>");
930
- sections.push(available.join("\n"));
931
- }
932
- if (invokedExplicitOnly.length > 0) {
933
- const userCallable = [
934
- "<user-callable-skills>",
935
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
936
- ];
937
- for (const skill of invokedExplicitOnly) {
938
- userCallable.push(...formatSkillEntry(skill));
939
- }
940
- userCallable.push("</user-callable-skills>");
941
- sections.push(userCallable.join("\n"));
942
- }
943
- return sections.length > 0 ? sections.join("\n") : null;
944
- }
945
- function formatActiveMcpCatalogsForPrompt(catalogs) {
946
- if (catalogs.length === 0) {
947
- return null;
948
- }
949
- const lines = [
950
- "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`."
951
- ];
952
- for (const catalog of catalogs) {
953
- lines.push(" <catalog>");
954
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
955
- lines.push(
956
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
957
- );
958
- lines.push(" </catalog>");
959
- }
960
- return lines.join("\n");
961
- }
962
- function formatToolGuidanceForPrompt(tools) {
963
- const guidedTools = tools.filter(
964
- (tool2) => Boolean(tool2.promptSnippet?.trim()) || (tool2.promptGuidelines?.length ?? 0) > 0
965
- );
966
- if (guidedTools.length === 0) {
967
- return null;
968
- }
969
- const lines = [];
970
- for (const tool2 of guidedTools) {
971
- lines.push(` <tool name="${escapeXml(tool2.name)}">`);
972
- if (tool2.promptSnippet?.trim()) {
973
- lines.push(` - ${escapeXml(tool2.promptSnippet.trim())}`);
974
- }
975
- if (tool2.promptGuidelines && tool2.promptGuidelines.length > 0) {
976
- for (const guideline of tool2.promptGuidelines) {
977
- lines.push(` - ${escapeXml(guideline)}`);
978
- }
979
- }
980
- lines.push(" </tool>");
981
- }
982
- return lines.join("\n");
983
- }
984
- function formatReferenceFilesLines() {
985
- const files = listReferenceFiles();
986
- if (files.length === 0) {
987
- return null;
988
- }
989
- return files.map((filePath) => {
990
- const name = path.basename(filePath);
991
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
992
- });
993
- }
994
- function formatArtifactsLines(artifactState) {
995
- if (!artifactState) return null;
996
- const lines = [];
997
- if (artifactState.lastCanvasId) {
998
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
999
- }
1000
- if (artifactState.lastCanvasUrl) {
1001
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1002
- }
1003
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1004
- lines.push("- recent_canvases:");
1005
- for (const canvas of artifactState.recentCanvases) {
1006
- lines.push(` - id: ${escapeXml(canvas.id)}`);
1007
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1008
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1009
- if (canvas.createdAt) {
1010
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1011
- }
1012
- }
1013
- }
1014
- if (artifactState.lastListId) {
1015
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1016
- }
1017
- if (artifactState.lastListUrl) {
1018
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
1019
- }
1020
- return lines.length > 0 ? lines : null;
1021
- }
1022
- function formatConfigurationLines(configuration) {
1023
- const keys = Object.keys(configuration ?? {}).sort(
1024
- (a, b) => a.localeCompare(b)
1025
- );
1026
- if (keys.length === 0) return null;
1027
- return keys.map(
1028
- (key) => `- ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
1029
- );
1030
- }
1031
- var HEADER = "You are a Slack-based helper assistant. Follow the personality block for voice and tone in every reply. The behavior and output blocks define platform mechanics and override personality only when those mechanics conflict.";
1032
- var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1033
- var TOOL_POLICY_RULES = [
1034
- "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
1035
- "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
1036
- "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1037
- "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
1038
- "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
1039
- `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
1040
- "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
1041
- "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1042
- "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1043
- ];
1044
- var TOOL_CALL_STYLE_RULES = [
1045
- "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1046
- "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1047
- "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
1048
- "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1049
- ];
1050
- var SKILL_POLICY_RULES = [
1051
- "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1052
- "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1053
- ];
1054
- var EXECUTION_CONTRACT_RULES = [
1055
- "- Actionable request: act in this turn.",
1056
- "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
1057
- "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
1058
- "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1059
- "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1060
- "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
1061
- ];
1062
- var CONVERSATION_RULES = [
1063
- "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1064
- "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1065
- "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1066
- ];
1067
- var SLACK_ACTION_RULES = [
1068
- "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1069
- "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1070
- "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1071
- "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
1072
- "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
1073
- "- Do not use reactions as progress indicators."
1074
- ];
1075
- var SAFETY_RULES = [
1076
- "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1077
- "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1078
- "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
1079
- ];
1080
- var FAILURE_RULES = [
1081
- "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1082
- "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
1083
- "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
1084
- ];
1085
- function renderRuleSection(tag, lines) {
1086
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
1087
- }
1088
- function buildBehaviorSection() {
1089
- return [
1090
- renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1091
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1092
- renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1093
- renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
1094
- renderRuleSection("conversation", CONVERSATION_RULES),
1095
- renderRuleSection("slack-actions", SLACK_ACTION_RULES),
1096
- renderRuleSection("safety", SAFETY_RULES),
1097
- renderRuleSection("failure-handling", FAILURE_RULES)
1098
- ].join("\n\n");
1099
- }
1100
- function buildOutputSection() {
1101
- const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
1102
- return [
1103
- openTag,
1104
- "- Start with the answer or result, not internal process narration.",
1105
- "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
1106
- "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
1107
- "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
1108
- "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
1109
- "</output>"
1110
- ].join("\n");
1111
- }
1112
- function buildIdentitySection() {
1113
- return renderTagBlock(
1114
- "identity",
1115
- `Your Slack username is \`${escapeXml(botConfig.userName)}\`.`
1116
- );
1117
- }
1118
- function buildRuntimeSection(params) {
1119
- const lines = [
1120
- params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
1121
- params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : ""
1122
- ].filter(Boolean);
1123
- if (lines.length === 0) {
1124
- return null;
1125
- }
1126
- return renderTagBlock("runtime", lines.join("\n"));
1127
- }
1128
- function buildContextSection(params) {
1129
- const blocks = [];
1130
- if (JUNIOR_WORLD) {
1131
- blocks.push(renderTag("world", [JUNIOR_WORLD.trim()]));
1132
- }
1133
- const referenceLines = formatReferenceFilesLines();
1134
- if (referenceLines) {
1135
- blocks.push(
1136
- renderTag("reference-files", [
1137
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
1138
- ...referenceLines
1139
- ])
1140
- );
1141
- }
1142
- const requesterLines = renderRequesterBlock({
1143
- full_name: params.requester?.fullName,
1144
- user_name: params.requester?.userName,
1145
- user_id: params.requester?.userId
1146
- });
1147
- if (requesterLines) {
1148
- blocks.push(requesterLines);
1149
- }
1150
- const artifactLines = formatArtifactsLines(params.artifactState);
1151
- if (artifactLines) {
1152
- blocks.push(renderTag("artifacts", artifactLines));
1153
- }
1154
- const configLines = formatConfigurationLines(params.configuration);
1155
- if (configLines) {
1156
- blocks.push(
1157
- renderTag("configuration", [
1158
- "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.",
1159
- ...configLines
1160
- ])
1161
- );
1162
- }
1163
- if (params.invocation) {
1164
- blocks.push(
1165
- renderTag("explicit-skill-trigger", [
1166
- "Treat this skill as selected. Load it unless the tool says it is unavailable.",
1167
- `/${escapeXml(params.invocation.skillName)}`
1168
- ])
1169
- );
1170
- }
1171
- const body = blocks.map((block) => block.join("\n")).join("\n\n");
1172
- if (!body) {
1173
- return null;
1174
- }
1175
- return renderTagBlock("context", body);
1176
- }
1177
- function buildCapabilitiesSection(params) {
1178
- const blocks = [];
1179
- const availableSkills = formatAvailableSkillsForPrompt(
1180
- params.availableSkills,
1181
- params.invocation
1182
- );
1183
- if (availableSkills) {
1184
- blocks.push(availableSkills);
1185
- }
1186
- const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1187
- params.activeMcpCatalogs
1188
- );
1189
- if (activeCatalogs) {
1190
- blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
1191
- }
1192
- const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
1193
- if (toolGuidance) {
1194
- blocks.push(renderTagBlock("tool-guidance", toolGuidance));
1195
- }
1196
- if (blocks.length === 0) {
1197
- return null;
1198
- }
1199
- return renderTagBlock("capabilities", blocks.join("\n\n"));
1200
- }
1201
- var STATIC_SYSTEM_PROMPT = [
1202
- HEADER,
1203
- buildIdentitySection(),
1204
- renderTagBlock("personality", JUNIOR_PERSONALITY.trim()),
1205
- renderTagBlock("behavior", buildBehaviorSection()),
1206
- buildOutputSection()
1207
- ].join("\n\n");
1208
- function buildSystemPrompt() {
1209
- return STATIC_SYSTEM_PROMPT;
1210
- }
1211
- function buildTurnContextPrompt(params) {
1212
- const includeSessionContext = params.includeSessionContext ?? true;
1213
- if (!includeSessionContext) {
1214
- return null;
1215
- }
1216
- const runtimeSections = [
1217
- buildCapabilitiesSection({
1218
- availableSkills: params.availableSkills,
1219
- activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1220
- invocation: params.invocation,
1221
- toolGuidance: params.toolGuidance ?? []
1222
- }),
1223
- buildContextSection({
1224
- requester: params.requester,
1225
- artifactState: params.artifactState,
1226
- configuration: params.configuration,
1227
- invocation: params.invocation
1228
- }),
1229
- buildRuntimeSection(params.runtime ?? {})
1230
- ].filter((section) => Boolean(section));
1231
- if (runtimeSections.length === 0) {
1232
- return null;
1233
- }
1234
- const sections = [
1235
- `<${TURN_CONTEXT_TAG}>`,
1236
- TURN_CONTEXT_HEADER,
1237
- "The current user instruction appears after this block in the same message.",
1238
- ...runtimeSections,
1239
- `</${TURN_CONTEXT_TAG}>`
1240
- ].filter((section) => Boolean(section));
1241
- return sections.join("\n\n");
1242
- }
1243
-
1244
643
  // src/chat/capabilities/catalog.ts
1245
644
  var cachedCatalog;
1246
645
  function getCapabilityCatalog() {
@@ -1635,13 +1034,13 @@ async function maybeExecuteJrRpcCustomCommand(command, deps) {
1635
1034
  }
1636
1035
 
1637
1036
  // src/chat/sandbox/skill-sandbox.ts
1638
- import fs2 from "fs/promises";
1639
- import path2 from "path";
1037
+ import fs from "fs/promises";
1038
+ import path from "path";
1640
1039
  var MAX_SKILL_FILE_BYTES = 256 * 1024;
1641
1040
  var DEFAULT_MAX_SKILL_FILE_CHARS = 2e4;
1642
1041
  var DEFAULT_MAX_SKILL_LIST_ENTRIES = 200;
1643
1042
  function normalizePathForOutput(value) {
1644
- return value.split(path2.sep).join("/");
1043
+ return value.split(path.sep).join("/");
1645
1044
  }
1646
1045
  function normalizeSkillName(value) {
1647
1046
  return value.trim().toLowerCase();
@@ -1650,12 +1049,12 @@ function resolvePathWithinRoot(root, relativePath) {
1650
1049
  if (!relativePath.trim()) {
1651
1050
  throw new Error("Path must not be empty.");
1652
1051
  }
1653
- if (path2.isAbsolute(relativePath)) {
1052
+ if (path.isAbsolute(relativePath)) {
1654
1053
  throw new Error("Absolute paths are not allowed.");
1655
1054
  }
1656
- const resolvedRoot = path2.resolve(root);
1657
- const resolvedPath = path2.resolve(resolvedRoot, relativePath);
1658
- if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path2.sep}`)) {
1055
+ const resolvedRoot = path.resolve(root);
1056
+ const resolvedPath = path.resolve(resolvedRoot, relativePath);
1057
+ if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)) {
1659
1058
  throw new Error("Path escapes the skill directory.");
1660
1059
  }
1661
1060
  return resolvedPath;
@@ -1735,9 +1134,9 @@ var SkillSandbox = class {
1735
1134
  1,
1736
1135
  Math.min(params.maxEntries ?? DEFAULT_MAX_SKILL_LIST_ENTRIES, 1e3)
1737
1136
  );
1738
- const root = path2.resolve(skill.skillPath);
1137
+ const root = path.resolve(skill.skillPath);
1739
1138
  const targetDirectory = resolvePathWithinRoot(root, directory);
1740
- const targetStats = await fs2.stat(targetDirectory);
1139
+ const targetStats = await fs.stat(targetDirectory);
1741
1140
  if (!targetStats.isDirectory()) {
1742
1141
  throw new Error(`Path is not a directory: ${directory}`);
1743
1142
  }
@@ -1746,14 +1145,14 @@ var SkillSandbox = class {
1746
1145
  let truncated = false;
1747
1146
  while (queue.length > 0) {
1748
1147
  const currentDirectory = queue.shift();
1749
- const children = await fs2.readdir(currentDirectory, {
1148
+ const children = await fs.readdir(currentDirectory, {
1750
1149
  withFileTypes: true
1751
1150
  });
1752
1151
  children.sort((a, b) => a.name.localeCompare(b.name));
1753
1152
  for (const child of children) {
1754
- const absolutePath = path2.join(currentDirectory, child.name);
1153
+ const absolutePath = path.join(currentDirectory, child.name);
1755
1154
  const relativePath = normalizePathForOutput(
1756
- path2.relative(root, absolutePath)
1155
+ path.relative(root, absolutePath)
1757
1156
  );
1758
1157
  if (!relativePath || relativePath.startsWith("..")) {
1759
1158
  continue;
@@ -1776,7 +1175,7 @@ var SkillSandbox = class {
1776
1175
  }
1777
1176
  }
1778
1177
  const relativeDirectory = normalizePathForOutput(
1779
- path2.relative(root, targetDirectory) || "."
1178
+ path.relative(root, targetDirectory) || "."
1780
1179
  );
1781
1180
  return {
1782
1181
  skillName: skill.name,
@@ -1791,9 +1190,9 @@ var SkillSandbox = class {
1791
1190
  1,
1792
1191
  Math.min(params.maxChars ?? DEFAULT_MAX_SKILL_FILE_CHARS, 1e5)
1793
1192
  );
1794
- const root = path2.resolve(skill.skillPath);
1193
+ const root = path.resolve(skill.skillPath);
1795
1194
  const targetPath = resolvePathWithinRoot(root, params.filePath);
1796
- const stats = await fs2.stat(targetPath);
1195
+ const stats = await fs.stat(targetPath);
1797
1196
  if (!stats.isFile()) {
1798
1197
  throw new Error(`Path is not a file: ${params.filePath}`);
1799
1198
  }
@@ -1802,11 +1201,11 @@ var SkillSandbox = class {
1802
1201
  `File exceeds ${MAX_SKILL_FILE_BYTES} bytes and cannot be loaded.`
1803
1202
  );
1804
1203
  }
1805
- const raw = await fs2.readFile(targetPath, "utf8");
1204
+ const raw = await fs.readFile(targetPath, "utf8");
1806
1205
  const truncated = raw.length > maxChars;
1807
1206
  return {
1808
1207
  skillName: skill.name,
1809
- path: normalizePathForOutput(path2.relative(root, targetPath)),
1208
+ path: normalizePathForOutput(path.relative(root, targetPath)),
1810
1209
  content: truncated ? raw.slice(0, maxChars) : raw,
1811
1210
  truncated
1812
1211
  };
@@ -2544,7 +1943,7 @@ function createBashTool() {
2544
1943
  }
2545
1944
 
2546
1945
  // src/chat/tools/sandbox/file-utils.ts
2547
- import path3 from "path";
1946
+ import path2 from "path";
2548
1947
 
2549
1948
  // src/chat/tools/execution/tool-input-error.ts
2550
1949
  var ToolInputError = class extends Error {
@@ -2640,12 +2039,12 @@ function matchesGlob(relativePath, pattern) {
2640
2039
  if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) {
2641
2040
  return true;
2642
2041
  }
2643
- return !pattern.includes("/") && matcher.test(path3.posix.basename(relativePath));
2042
+ return !pattern.includes("/") && matcher.test(path2.posix.basename(relativePath));
2644
2043
  }
2645
2044
  function resolveWorkspacePath(input, fallback = ".") {
2646
2045
  const requested = (input ?? "").trim() || fallback;
2647
- const absolute = requested.startsWith("/") ? requested : path3.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
2648
- const normalized = path3.posix.normalize(absolute);
2046
+ const absolute = requested.startsWith("/") ? requested : path2.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
2047
+ const normalized = path2.posix.normalize(absolute);
2649
2048
  if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
2650
2049
  throw new ToolInputError(
2651
2050
  `Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
@@ -2674,7 +2073,7 @@ async function collectFiles(params) {
2674
2073
  throw error;
2675
2074
  }
2676
2075
  for (const entry of entries) {
2677
- const fullPath = path3.posix.join(dirPath, entry);
2076
+ const fullPath = path2.posix.join(dirPath, entry);
2678
2077
  let stat2;
2679
2078
  try {
2680
2079
  stat2 = await params.fs.stat(fullPath);
@@ -2692,7 +2091,7 @@ async function collectFiles(params) {
2692
2091
  if (limitReached) return;
2693
2092
  continue;
2694
2093
  }
2695
- const relativePath = path3.posix.relative(params.root, fullPath);
2094
+ const relativePath = path2.posix.relative(params.root, fullPath);
2696
2095
  if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
2697
2096
  files.push(fullPath);
2698
2097
  if (params.limit && files.length >= params.limit) {
@@ -2717,7 +2116,7 @@ async function collectFiles(params) {
2717
2116
  throw error;
2718
2117
  }
2719
2118
  if (!stat.isDirectory()) {
2720
- const relativePath = path3.posix.basename(params.root);
2119
+ const relativePath = path2.posix.basename(params.root);
2721
2120
  return {
2722
2121
  files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
2723
2122
  limitReached: false,
@@ -2994,7 +2393,7 @@ function createEditFileTool() {
2994
2393
  }
2995
2394
 
2996
2395
  // src/chat/tools/sandbox/find-files.ts
2997
- import path4 from "path";
2396
+ import path3 from "path";
2998
2397
  import { Type as Type3 } from "@sinclair/typebox";
2999
2398
  var DEFAULT_FIND_LIMIT = 1e3;
3000
2399
  async function findFiles(params) {
@@ -3016,7 +2415,7 @@ async function findFiles(params) {
3016
2415
  });
3017
2416
  }
3018
2417
  const relativePaths = files.map(
3019
- (filePath) => path4.posix.relative(root, filePath)
2418
+ (filePath) => path3.posix.relative(root, filePath)
3020
2419
  );
3021
2420
  const bounded = truncateText(
3022
2421
  relativePaths.length > 0 ? relativePaths.join("\n") : "No files found matching pattern"
@@ -3081,7 +2480,7 @@ function createFindFilesTool() {
3081
2480
  }
3082
2481
 
3083
2482
  // src/chat/tools/sandbox/grep.ts
3084
- import path5 from "path";
2483
+ import path4 from "path";
3085
2484
  import { Type as Type4 } from "@sinclair/typebox";
3086
2485
  var DEFAULT_GREP_LIMIT = 100;
3087
2486
  var MAX_GREP_LINE_CHARS = 500;
@@ -3153,7 +2552,7 @@ async function grepFiles(params) {
3153
2552
  continue;
3154
2553
  }
3155
2554
  const lines = normalizeToLf(content).split("\n");
3156
- const relativePath = files.length === 1 && filePath === root ? path5.posix.basename(filePath) : path5.posix.relative(root, filePath);
2555
+ const relativePath = files.length === 1 && filePath === root ? path4.posix.basename(filePath) : path4.posix.relative(root, filePath);
3157
2556
  const matchedLines = [];
3158
2557
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3159
2558
  if (!lineMatches({
@@ -3280,7 +2679,7 @@ function createGrepTool() {
3280
2679
  }
3281
2680
 
3282
2681
  // src/chat/tools/sandbox/attach-file.ts
3283
- import path6 from "path";
2682
+ import path5 from "path";
3284
2683
  import { Type as Type5 } from "@sinclair/typebox";
3285
2684
  var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
3286
2685
  var MIME_BY_EXTENSION = {
@@ -3302,20 +2701,20 @@ function normalizeSandboxPath(inputPath) {
3302
2701
  if (!trimmed) {
3303
2702
  throw new Error("path is required");
3304
2703
  }
3305
- if (path6.posix.isAbsolute(trimmed)) {
2704
+ if (path5.posix.isAbsolute(trimmed)) {
3306
2705
  return trimmed;
3307
2706
  }
3308
- return path6.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
2707
+ return path5.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
3309
2708
  }
3310
2709
  function sanitizeFilename(value, fallbackPath) {
3311
2710
  const candidate = (value ?? "").trim();
3312
2711
  if (candidate) {
3313
- const base = path6.posix.basename(candidate);
2712
+ const base = path5.posix.basename(candidate);
3314
2713
  if (base && base !== "." && base !== "..") {
3315
2714
  return base;
3316
2715
  }
3317
2716
  }
3318
- const derived = path6.posix.basename(fallbackPath);
2717
+ const derived = path5.posix.basename(fallbackPath);
3319
2718
  if (derived && derived !== "." && derived !== "..") {
3320
2719
  return derived;
3321
2720
  }
@@ -3326,7 +2725,7 @@ function inferMimeType(filename, explicitMimeType) {
3326
2725
  if (explicit) {
3327
2726
  return explicit;
3328
2727
  }
3329
- const ext = path6.extname(filename).toLowerCase();
2728
+ const ext = path5.extname(filename).toLowerCase();
3330
2729
  return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
3331
2730
  }
3332
2731
  async function detectMimeType(sandbox, targetPath) {
@@ -3373,7 +2772,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
3373
2772
  const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
3374
2773
  if (!fileBuffer) {
3375
2774
  const generatedFile = hooks.getGeneratedFile?.(
3376
- path6.posix.basename(targetPath)
2775
+ path5.posix.basename(targetPath)
3377
2776
  );
3378
2777
  if (generatedFile) {
3379
2778
  hooks.onGeneratedFiles?.([generatedFile]);
@@ -3421,7 +2820,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
3421
2820
  }
3422
2821
 
3423
2822
  // src/chat/tools/sandbox/list-dir.ts
3424
- import path7 from "path";
2823
+ import path6 from "path";
3425
2824
  import { Type as Type6 } from "@sinclair/typebox";
3426
2825
  var DEFAULT_LIST_LIMIT = 500;
3427
2826
  async function listDir(params) {
@@ -3457,7 +2856,7 @@ async function listDir(params) {
3457
2856
  entryLimitReached = true;
3458
2857
  break;
3459
2858
  }
3460
- const entryPath = path7.posix.join(dirPath, entry);
2859
+ const entryPath = path6.posix.join(dirPath, entry);
3461
2860
  try {
3462
2861
  const entryStat = await params.fs.stat(entryPath);
3463
2862
  output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
@@ -4116,11 +3515,11 @@ function sliceFileContent(params) {
4116
3515
  } : {}
4117
3516
  };
4118
3517
  }
4119
- function missingFileResult(path10) {
3518
+ function missingFileResult(path9) {
4120
3519
  return {
4121
3520
  content: "",
4122
3521
  error: "not_found",
4123
- path: path10,
3522
+ path: path9,
4124
3523
  success: false
4125
3524
  };
4126
3525
  }
@@ -6585,15 +5984,22 @@ function summarizeMessageText(text) {
6585
5984
  }
6586
5985
  return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
6587
5986
  }
5987
+ function isStructuredThreadContext(context) {
5988
+ return /^<thread-(compactions|transcript)>/.test(context);
5989
+ }
5990
+ function renderThreadContextForPrompt(context) {
5991
+ if (isStructuredThreadContext(context)) {
5992
+ return context;
5993
+ }
5994
+ return ["<thread-background>", context, "</thread-background>"].join("\n");
5995
+ }
6588
5996
  function buildUserTurnText(userInput, conversationContext) {
6589
5997
  const trimmedContext = conversationContext?.trim();
6590
5998
  if (!trimmedContext) {
6591
5999
  return userInput;
6592
6000
  }
6593
6001
  return [
6594
- "<thread-background>",
6595
- trimmedContext,
6596
- "</thread-background>",
6002
+ renderThreadContextForPrompt(trimmedContext),
6597
6003
  "",
6598
6004
  "<current-instruction>",
6599
6005
  userInput,
@@ -7881,7 +7287,7 @@ function createTracedStreamFn(baseOrOptions = streamSimple) {
7881
7287
  }
7882
7288
 
7883
7289
  // src/chat/sandbox/sandbox.ts
7884
- import fs4 from "fs/promises";
7290
+ import fs3 from "fs/promises";
7885
7291
 
7886
7292
  // src/chat/oauth-flow.ts
7887
7293
  import { randomBytes } from "crypto";
@@ -8205,8 +7611,8 @@ function sandboxProxyUrl(requesterToken) {
8205
7611
  "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
8206
7612
  );
8207
7613
  }
8208
- const path10 = requesterToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${requesterToken}` : SANDBOX_EGRESS_PROXY_PATH;
8209
- return new URL(path10, baseUrl).toString();
7614
+ const path9 = requesterToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${requesterToken}` : SANDBOX_EGRESS_PROXY_PATH;
7615
+ return new URL(path9, baseUrl).toString();
8210
7616
  }
8211
7617
  function buildSandboxEgressNetworkPolicy(input) {
8212
7618
  const allow = {
@@ -8453,20 +7859,20 @@ import { Sandbox } from "@vercel/sandbox";
8453
7859
  import { createBashTool as createBashTool2 } from "bash-tool";
8454
7860
 
8455
7861
  // src/chat/sandbox/skill-sync.ts
8456
- import fs3 from "fs/promises";
8457
- import path8 from "path";
7862
+ import fs2 from "fs/promises";
7863
+ import path7 from "path";
8458
7864
  function toPosixRelative(base, absolute) {
8459
- return path8.relative(base, absolute).split(path8.sep).join("/");
7865
+ return path7.relative(base, absolute).split(path7.sep).join("/");
8460
7866
  }
8461
7867
  async function listFilesRecursive(root) {
8462
7868
  const queue = [root];
8463
7869
  const files = [];
8464
7870
  while (queue.length > 0) {
8465
7871
  const dir = queue.shift();
8466
- const entries = await fs3.readdir(dir, { withFileTypes: true });
7872
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
8467
7873
  entries.sort((a, b) => a.name.localeCompare(b.name));
8468
7874
  for (const entry of entries) {
8469
- const absolute = path8.join(dir, entry.name);
7875
+ const absolute = path7.join(dir, entry.name);
8470
7876
  if (entry.isDirectory()) {
8471
7877
  queue.push(absolute);
8472
7878
  } else if (entry.isFile()) {
@@ -8490,7 +7896,7 @@ async function buildSkillSyncFiles(availableSkills, referenceFiles) {
8490
7896
  }
8491
7897
  filesToWrite.push({
8492
7898
  path: `${sandboxSkillDir(skill.name)}/${relative}`,
8493
- content: await fs3.readFile(absoluteFile)
7899
+ content: await fs2.readFile(absoluteFile)
8494
7900
  });
8495
7901
  }
8496
7902
  index.skills.push({
@@ -8505,10 +7911,10 @@ async function buildSkillSyncFiles(availableSkills, referenceFiles) {
8505
7911
  });
8506
7912
  if (referenceFiles && referenceFiles.length > 0) {
8507
7913
  for (const absoluteFile of referenceFiles) {
8508
- const fileName = path8.basename(absoluteFile);
7914
+ const fileName = path7.basename(absoluteFile);
8509
7915
  filesToWrite.push({
8510
7916
  path: `${SANDBOX_DATA_ROOT}/${fileName}`,
8511
- content: await fs3.readFile(absoluteFile)
7917
+ content: await fs2.readFile(absoluteFile)
8512
7918
  });
8513
7919
  }
8514
7920
  }
@@ -8517,7 +7923,7 @@ async function buildSkillSyncFiles(availableSkills, referenceFiles) {
8517
7923
  function collectDirectories(filesToWrite, workspaceRoot) {
8518
7924
  const directoriesToEnsure = /* @__PURE__ */ new Set();
8519
7925
  for (const file of filesToWrite) {
8520
- const normalizedPath = path8.posix.normalize(file.path);
7926
+ const normalizedPath = path7.posix.normalize(file.path);
8521
7927
  const parts = normalizedPath.split("/").filter(Boolean);
8522
7928
  let current = "";
8523
7929
  for (let index = 0; index < parts.length - 1; index += 1) {
@@ -8530,19 +7936,19 @@ function collectDirectories(filesToWrite, workspaceRoot) {
8530
7936
  ).sort((a, b) => a.length - b.length);
8531
7937
  }
8532
7938
  function resolveHostSkillPath(availableSkills, sandboxPath) {
8533
- const normalizedPath = path8.posix.normalize(sandboxPath.trim());
7939
+ const normalizedPath = path7.posix.normalize(sandboxPath.trim());
8534
7940
  for (const skill of availableSkills) {
8535
7941
  const virtualRoot = sandboxSkillDir(skill.name);
8536
7942
  if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
8537
7943
  continue;
8538
7944
  }
8539
- const relativePath = path8.posix.relative(virtualRoot, normalizedPath);
7945
+ const relativePath = path7.posix.relative(virtualRoot, normalizedPath);
8540
7946
  if (!relativePath || relativePath.startsWith("../")) {
8541
7947
  return null;
8542
7948
  }
8543
- const hostRoot = path8.resolve(skill.skillPath);
8544
- const hostPath = path8.resolve(hostRoot, ...relativePath.split("/"));
8545
- if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path8.sep}`)) {
7949
+ const hostRoot = path7.resolve(skill.skillPath);
7950
+ const hostPath = path7.resolve(hostRoot, ...relativePath.split("/"));
7951
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path7.sep}`)) {
8546
7952
  return null;
8547
7953
  }
8548
7954
  return hostPath;
@@ -8550,16 +7956,16 @@ function resolveHostSkillPath(availableSkills, sandboxPath) {
8550
7956
  return null;
8551
7957
  }
8552
7958
  function resolveHostDataPath(referenceFiles, sandboxPath) {
8553
- const normalizedPath = path8.posix.normalize(sandboxPath.trim());
7959
+ const normalizedPath = path7.posix.normalize(sandboxPath.trim());
8554
7960
  if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
8555
7961
  return null;
8556
7962
  }
8557
- const relativePath = path8.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
7963
+ const relativePath = path7.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
8558
7964
  if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
8559
7965
  return null;
8560
7966
  }
8561
7967
  for (const hostFile of referenceFiles) {
8562
- if (path8.basename(hostFile) === relativePath) {
7968
+ if (path7.basename(hostFile) === relativePath) {
8563
7969
  return hostFile;
8564
7970
  }
8565
7971
  }
@@ -8619,6 +8025,7 @@ async function syncSkillsToSandbox(params) {
8619
8025
 
8620
8026
  // src/chat/sandbox/session.ts
8621
8027
  var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
8028
+ var DEFAULT_BASH_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
8622
8029
  var SANDBOX_RUNTIME = "node22";
8623
8030
  var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
8624
8031
  var SNAPSHOT_BOOT_RETRY_COUNT = 3;
@@ -8691,6 +8098,16 @@ function getCommandStreamInterruptedResult() {
8691
8098
  stderrTruncated: false
8692
8099
  };
8693
8100
  }
8101
+ function getCommandAbortedResult() {
8102
+ return {
8103
+ stdout: "",
8104
+ stderr: "Command aborted because the agent turn was cancelled.",
8105
+ exitCode: 130,
8106
+ stdoutTruncated: false,
8107
+ stderrTruncated: false,
8108
+ aborted: true
8109
+ };
8110
+ }
8694
8111
  function createSandboxSessionManager(options) {
8695
8112
  let sandbox = null;
8696
8113
  let sandboxIdHint = options?.sandboxId;
@@ -8946,7 +8363,9 @@ function createSandboxSessionManager(options) {
8946
8363
  "sandbox_hint_discarded_profile_mismatch",
8947
8364
  traceContext,
8948
8365
  {
8949
- ...options?.sandboxDependencyProfileHash ? { "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash } : {},
8366
+ ...options?.sandboxDependencyProfileHash ? {
8367
+ "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
8368
+ } : {},
8950
8369
  ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8951
8370
  },
8952
8371
  "Dependency profile changed; discarding sandbox hint and creating fresh session"
@@ -9114,37 +8533,63 @@ function createSandboxSessionManager(options) {
9114
8533
  return {
9115
8534
  bash: async (input) => {
9116
8535
  let timedOut = false;
8536
+ let aborted = false;
9117
8537
  let timeoutId;
8538
+ let onAbort;
9118
8539
  try {
8540
+ if (input.signal?.aborted) {
8541
+ return getCommandAbortedResult();
8542
+ }
9119
8543
  await refreshNetworkPolicy(sandboxInstance);
8544
+ if (input.signal?.aborted) {
8545
+ return getCommandAbortedResult();
8546
+ }
9120
8547
  const sandboxCommandEnv = await resolveCommandEnv();
8548
+ if (input.signal?.aborted) {
8549
+ return getCommandAbortedResult();
8550
+ }
9121
8551
  const script = buildNonInteractiveShellScript(input.command, {
9122
8552
  env: { ...sandboxCommandEnv, ...input.env ?? {} },
9123
8553
  pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
9124
8554
  });
9125
- const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
9126
- timeoutId = controller ? setTimeout(() => {
8555
+ const controller = new AbortController();
8556
+ const timeoutMs2 = input.timeoutMs && input.timeoutMs > 0 ? input.timeoutMs : DEFAULT_BASH_COMMAND_TIMEOUT_MS;
8557
+ onAbort = () => {
8558
+ aborted = true;
8559
+ controller.abort(input.signal?.reason);
8560
+ };
8561
+ if (input.signal) {
8562
+ input.signal.addEventListener("abort", onAbort, { once: true });
8563
+ if (input.signal.aborted) {
8564
+ onAbort();
8565
+ }
8566
+ }
8567
+ timeoutId = setTimeout(() => {
9127
8568
  timedOut = true;
9128
8569
  controller.abort();
9129
- }, input.timeoutMs) : void 0;
8570
+ }, timeoutMs2);
8571
+ timeoutId.unref?.();
9130
8572
  const commandResult2 = await sandboxInstance.runCommand({
9131
8573
  cmd: "bash",
9132
8574
  args: ["-c", script],
9133
8575
  cwd: SANDBOX_WORKSPACE_ROOT,
9134
- ...controller ? { signal: controller.signal } : {}
8576
+ signal: controller.signal
9135
8577
  });
9136
8578
  return await readCommandOutput(commandResult2);
9137
8579
  } catch (error) {
9138
8580
  if (timedOut) {
9139
8581
  return {
9140
8582
  stdout: "",
9141
- stderr: `Command timed out after ${input.timeoutMs}ms`,
8583
+ stderr: `Command timed out after ${input.timeoutMs && input.timeoutMs > 0 ? input.timeoutMs : DEFAULT_BASH_COMMAND_TIMEOUT_MS}ms`,
9142
8584
  exitCode: 124,
9143
8585
  stdoutTruncated: false,
9144
8586
  stderrTruncated: false,
9145
8587
  timedOut: true
9146
8588
  };
9147
8589
  }
8590
+ if (aborted || input.signal?.aborted) {
8591
+ return getCommandAbortedResult();
8592
+ }
9148
8593
  if (isSandboxCommandStreamInterruptedError(error)) {
9149
8594
  return getCommandStreamInterruptedResult();
9150
8595
  }
@@ -9153,6 +8598,9 @@ function createSandboxSessionManager(options) {
9153
8598
  if (timeoutId) {
9154
8599
  clearTimeout(timeoutId);
9155
8600
  }
8601
+ if (input.signal && onAbort) {
8602
+ input.signal.removeEventListener("abort", onAbort);
8603
+ }
9156
8604
  }
9157
8605
  },
9158
8606
  readFile: async (input) => await executeReadFile(input, {
@@ -9328,7 +8776,7 @@ function createSandboxExecutor(options) {
9328
8776
  "Sandbox boot requested"
9329
8777
  );
9330
8778
  };
9331
- const executeBashTool = async (rawInput, command) => {
8779
+ const executeBashTool = async (rawInput, command, signal) => {
9332
8780
  const env = parseEnv(rawInput.env);
9333
8781
  const timeoutMs = positiveInteger(rawInput.timeoutMs);
9334
8782
  logSandboxBootRequest("tool.bash", {
@@ -9346,7 +8794,8 @@ function createSandboxExecutor(options) {
9346
8794
  const response = await executeBash({
9347
8795
  command,
9348
8796
  ...env ? { env } : {},
9349
- ...timeoutMs ? { timeoutMs } : {}
8797
+ ...timeoutMs ? { timeoutMs } : {},
8798
+ ...signal ? { signal } : {}
9350
8799
  });
9351
8800
  setSpanAttributes({
9352
8801
  "process.exit.code": response.exitCode,
@@ -9397,7 +8846,7 @@ function createSandboxExecutor(options) {
9397
8846
  const hostPath = resolveHostSkillPath(availableSkills, filePath) ?? resolveHostDataPath(referenceFiles, filePath);
9398
8847
  if (hostPath) {
9399
8848
  try {
9400
- const content = await fs4.readFile(hostPath, "utf8");
8849
+ const content = await fs3.readFile(hostPath, "utf8");
9401
8850
  setSpanAttributes({
9402
8851
  "app.sandbox.path.length": filePath.length,
9403
8852
  "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
@@ -9616,7 +9065,7 @@ function createSandboxExecutor(options) {
9616
9065
  return { result: custom.result };
9617
9066
  }
9618
9067
  }
9619
- return await executeBashTool(rawInput, bashCommand);
9068
+ return await executeBashTool(rawInput, bashCommand, params.signal);
9620
9069
  }
9621
9070
  try {
9622
9071
  if (params.toolName === "readFile") {
@@ -10455,7 +9904,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10455
9904
  parameters: toolDef.inputSchema,
10456
9905
  prepareArguments: toolDef.prepareArguments,
10457
9906
  executionMode: toolDef.executionMode,
10458
- execute: async (toolCallId, params) => {
9907
+ execute: async (toolCallId, params, signal) => {
10459
9908
  const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
10460
9909
  const toolArgumentsAttribute = serializeToolPayload(params);
10461
9910
  const toolArgumentsMetadata = toGenAiPayloadTraceAttributes(
@@ -10503,9 +9952,11 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10503
9952
  const isSandbox = Boolean(sandboxExecutor?.canExecute(toolName));
10504
9953
  const result = isSandbox ? await sandboxExecutor.execute({
10505
9954
  toolName,
10506
- input: sandboxInput
9955
+ input: sandboxInput,
9956
+ ...signal ? { signal } : {}
10507
9957
  }) : await toolDef.execute(toolInput, {
10508
- experimental_context: sandbox
9958
+ experimental_context: sandbox,
9959
+ ...signal ? { signal } : {}
10509
9960
  });
10510
9961
  const normalized = normalizeToolResult(result, isSandbox);
10511
9962
  if (bashCommand && pluginAuthOrchestration) {
@@ -11484,13 +10935,13 @@ function buildClassifierSystemPrompt() {
11484
10935
  "Choose exactly one bucket: none, low, medium, high, or xhigh.",
11485
10936
  "",
11486
10937
  "Use none only for greetings, acknowledgments, and turns that need no substantive assistant work.",
11487
- "Use low rarely: only for deterministic one-step answers or transformations with no tools, no current/external facts, no thread-background interpretation, and no source verification.",
10938
+ "Use low rarely: only for deterministic one-step answers or transformations with no tools, no current/external facts, no prior thread-context interpretation, and no source verification.",
11488
10939
  "Use medium for normal assistant work: explanations, source-backed checks, thread follow-ups, tool choice, likely tool use, ambiguous asks, multi-step analysis, or anything where a confident but shallow answer would be risky.",
11489
10940
  "Use high for research-heavy work, non-trivial drafting, or explicit requests to be thorough.",
11490
10941
  "Use xhigh for the most complex tasks: code changes, debugging/root-cause analysis, broad refactors, architecture decisions, multi-file implementation, or any task where deep reasoning across multiple systems or files is required.",
11491
10942
  "When unsure between two non-none buckets, choose the higher bucket. Do not use low as the default.",
11492
10943
  "",
11493
- "Classify based on the substance of the task, not the length of the current message. When the current instruction is a short affirmation (for example: 'go', 'do it', 'yes please', 'proceed') and the thread-background contains a pending task, classify the pending task \u2014 not the affirmation.",
10944
+ "Classify based on the substance of the task, not the length of the current message. When the current instruction is a short affirmation (for example: 'go', 'do it', 'yes please', 'proceed') and prior thread context contains a pending task, classify the pending task \u2014 not the affirmation.",
11494
10945
  "",
11495
10946
  "Return JSON only with thinking_level, confidence, and reason.",
11496
10947
  "confidence must be a number from 0 to 1, not a word label."
@@ -11499,12 +10950,17 @@ function buildClassifierSystemPrompt() {
11499
10950
  function buildClassifierPrompt(args) {
11500
10951
  const sections = [];
11501
10952
  if (args.conversationContext) {
11502
- sections.push(
11503
- "<thread-background>",
11504
- args.conversationContext.text,
11505
- "</thread-background>",
11506
- ""
11507
- );
10953
+ const contextText = args.conversationContext.text;
10954
+ if (/^<thread-(compactions|transcript)>/.test(contextText)) {
10955
+ sections.push(contextText, "");
10956
+ } else {
10957
+ sections.push(
10958
+ "<thread-background>",
10959
+ contextText,
10960
+ "</thread-background>",
10961
+ ""
10962
+ );
10963
+ }
11508
10964
  }
11509
10965
  sections.push(
11510
10966
  "<current-instruction>",
@@ -12296,9 +11752,38 @@ function createMcpAuthOrchestration(deps, abortAgent) {
12296
11752
 
12297
11753
  // src/chat/respond.ts
12298
11754
  var PROVIDER_RETRY_DELAYS_MS = [1e3, 2e3];
11755
+ var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
12299
11756
  function sleep3(ms) {
12300
11757
  return new Promise((resolve) => setTimeout(resolve, ms));
12301
11758
  }
11759
+ function waitForAbortSettlement(promise, timeoutMs) {
11760
+ return new Promise((resolve) => {
11761
+ let done = false;
11762
+ const timeoutId = setTimeout(() => {
11763
+ if (!done) {
11764
+ done = true;
11765
+ resolve(false);
11766
+ }
11767
+ }, timeoutMs);
11768
+ timeoutId.unref?.();
11769
+ promise.then(
11770
+ () => {
11771
+ if (!done) {
11772
+ done = true;
11773
+ clearTimeout(timeoutId);
11774
+ resolve(true);
11775
+ }
11776
+ },
11777
+ () => {
11778
+ if (!done) {
11779
+ done = true;
11780
+ clearTimeout(timeoutId);
11781
+ resolve(true);
11782
+ }
11783
+ }
11784
+ );
11785
+ });
11786
+ }
12302
11787
  var startupDiscoveryLogged = false;
12303
11788
  var MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS = 2e3;
12304
11789
  function buildOmittedImageAttachmentNotice(count) {
@@ -12865,8 +12350,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12865
12350
  includeSessionContext,
12866
12351
  toolGuidance,
12867
12352
  runtime: {
12868
- conversationId: spanContext.conversationId,
12869
- traceId: getActiveTraceId()
12353
+ conversationId: spanContext.conversationId
12870
12354
  },
12871
12355
  invocation: skillInvocation,
12872
12356
  requester: context.requester,
@@ -13054,8 +12538,20 @@ async function generateAssistantReply(messageText2, context = {}) {
13054
12538
  },
13055
12539
  "Agent turn timed out and was aborted"
13056
12540
  );
13057
- await run2.catch(() => {
13058
- });
12541
+ const settled = await waitForAbortSettlement(
12542
+ run2,
12543
+ AGENT_ABORT_SETTLE_GRACE_MS
12544
+ );
12545
+ if (!settled) {
12546
+ logWarn(
12547
+ "agent_turn_abort_settle_timeout",
12548
+ {},
12549
+ {
12550
+ "app.ai.abort_settle_grace_ms": AGENT_ABORT_SETTLE_GRACE_MS
12551
+ },
12552
+ "Timed-out agent run did not settle after abort before resume snapshot"
12553
+ );
12554
+ }
13059
12555
  timeoutResumeMessages = [...agent.state.messages];
13060
12556
  }
13061
12557
  if (getPendingAuthPause()) {
@@ -13447,20 +12943,25 @@ function buildConversationContext(conversation, options = {}) {
13447
12943
  " </compaction>"
13448
12944
  );
13449
12945
  }
13450
- lines.push("</thread-compactions>", "");
12946
+ lines.push("</thread-compactions>");
13451
12947
  }
13452
- lines.push("<thread-transcript>");
13453
- for (const [index, message] of messages.entries()) {
13454
- const author = escapeXml(message.author?.userName ?? message.role);
13455
- const ts = new Date(message.createdAtMs).toISOString();
13456
- const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
13457
- lines.push(
13458
- ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${slackTsAttr}>`,
13459
- renderConversationMessageLine(message, conversation),
13460
- " </message>"
13461
- );
12948
+ if (messages.length > 0) {
12949
+ if (lines.length > 0) {
12950
+ lines.push("");
12951
+ }
12952
+ lines.push("<thread-transcript>");
12953
+ for (const [index, message] of messages.entries()) {
12954
+ const author = escapeXml(message.author?.userName ?? message.role);
12955
+ const ts = new Date(message.createdAtMs).toISOString();
12956
+ const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
12957
+ lines.push(
12958
+ ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${slackTsAttr}>`,
12959
+ renderConversationMessageLine(message, conversation),
12960
+ " </message>"
12961
+ );
12962
+ }
12963
+ lines.push("</thread-transcript>");
13462
12964
  }
13463
- lines.push("</thread-transcript>");
13464
12965
  return lines.join("\n");
13465
12966
  }
13466
12967
  function pruneCompactions(compactions) {
@@ -13700,7 +13201,7 @@ function buildSlackReplyFooter(args) {
13700
13201
  label: "ID",
13701
13202
  value: conversationId
13702
13203
  };
13703
- const conversationUrl = buildSentryConversationUrl(conversationId);
13204
+ const conversationUrl = getAgentPluginSlackConversationLink(conversationId)?.url ?? buildSentryConversationUrl(conversationId);
13704
13205
  if (conversationUrl) {
13705
13206
  idItem.url = conversationUrl;
13706
13207
  }
@@ -16465,8 +15966,8 @@ async function GET3(request, provider, waitUntil) {
16465
15966
  import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS5 } from "chat";
16466
15967
 
16467
15968
  // src/chat/slack/app-home.ts
16468
- import fs5 from "fs";
16469
- import path9 from "path";
15969
+ import fs4 from "fs";
15970
+ import path8 from "path";
16470
15971
  var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
16471
15972
  var MAX_HOME_SKILLS = 6;
16472
15973
  var MAX_SECTION_TEXT_CHARS = 3e3;
@@ -16478,9 +15979,9 @@ function clampSectionText(text) {
16478
15979
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
16479
15980
  }
16480
15981
  function loadDescriptionText() {
16481
- const descriptionPath = path9.join(homeDir(), "DESCRIPTION.md");
15982
+ const descriptionPath = path8.join(homeDir(), "DESCRIPTION.md");
16482
15983
  try {
16483
- const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
15984
+ const raw = fs4.readFileSync(descriptionPath, "utf8").trim();
16484
15985
  if (raw.length > 0) {
16485
15986
  return clampSectionText(raw);
16486
15987
  }
@@ -17308,12 +16809,12 @@ function normalizePort(value) {
17308
16809
  function sandboxIdFromPayload(payload) {
17309
16810
  return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
17310
16811
  }
17311
- function normalizedForwardedPath(path10) {
17312
- if (!path10.startsWith("/") || path10.startsWith("//") || path10.includes("#") || /[\r\n]/.test(path10)) {
16812
+ function normalizedForwardedPath(path9) {
16813
+ if (!path9.startsWith("/") || path9.startsWith("//") || path9.includes("#") || /[\r\n]/.test(path9)) {
17313
16814
  return { ok: false, error: "Invalid forwarded path" };
17314
16815
  }
17315
16816
  try {
17316
- const url = new URL(path10, "https://sandbox-forwarded.local");
16817
+ const url = new URL(path9, "https://sandbox-forwarded.local");
17317
16818
  return { ok: true, path: `${url.pathname}${url.search}` };
17318
16819
  } catch {
17319
16820
  return { ok: false, error: "Invalid forwarded path" };
@@ -17348,13 +16849,13 @@ function buildUpstreamUrl(request) {
17348
16849
  if (forwardedPort && !port) {
17349
16850
  return { ok: false, error: "Invalid forwarded port" };
17350
16851
  }
17351
- const path10 = upstreamPath(request);
17352
- if (!path10.ok) {
17353
- return { ok: false, error: path10.error };
16852
+ const path9 = upstreamPath(request);
16853
+ if (!path9.ok) {
16854
+ return { ok: false, error: path9.error };
17354
16855
  }
17355
16856
  try {
17356
16857
  const url = new URL(
17357
- `${scheme}://${host}${port ? `:${port}` : ""}${path10.path}`
16858
+ `${scheme}://${host}${port ? `:${port}` : ""}${path9.path}`
17358
16859
  );
17359
16860
  return { ok: true, url };
17360
16861
  } catch {
@@ -20117,7 +19618,7 @@ function createReplyToThread(deps) {
20117
19618
  if (conversationId && loadedPiMessages.canCompact && piMessages?.length) {
20118
19619
  const compaction = await deps.services.contextCompactor.maybeCompact({
20119
19620
  conversation: preparedState.conversation,
20120
- conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
19621
+ conversationContext: preparedState.conversationContext,
20121
19622
  conversationId,
20122
19623
  metadata: {
20123
19624
  threadId,
@@ -20159,7 +19660,7 @@ function createReplyToThread(deps) {
20159
19660
  fullName: message.author.fullName ?? fallbackIdentity?.fullName,
20160
19661
  email: fallbackIdentity?.email
20161
19662
  },
20162
- conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
19663
+ conversationContext: preparedState.conversationContext,
20163
19664
  artifactState: preparedState.artifacts,
20164
19665
  piMessages,
20165
19666
  pendingAuth: preparedState.conversation.processing.pendingAuth,
@@ -20720,8 +20221,7 @@ function createPrepareTurnState(deps) {
20720
20221
  requesterId: args.context.requesterId,
20721
20222
  runId: args.context.runId
20722
20223
  });
20723
- const conversationContext = buildConversationContext(conversation);
20724
- const routingContext = buildConversationContext(conversation, {
20224
+ const conversationContext = buildConversationContext(conversation, {
20725
20225
  excludeMessageId: userMessageId
20726
20226
  });
20727
20227
  setSpanAttributes({
@@ -20736,7 +20236,6 @@ function createPrepareTurnState(deps) {
20736
20236
  sandboxId: existingSandboxId,
20737
20237
  sandboxDependencyProfileHash: existingSandboxDependencyProfileHash,
20738
20238
  conversationContext,
20739
- routingContext,
20740
20239
  userMessageId
20741
20240
  };
20742
20241
  };
@@ -20783,7 +20282,7 @@ function createSlackRuntime(options) {
20783
20282
  conversation: preparedState.conversation
20784
20283
  });
20785
20284
  },
20786
- getPreparedConversationContext: (preparedState) => preparedState.routingContext ?? preparedState.conversationContext,
20285
+ getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
20787
20286
  decideSubscribedReply: services.subscribedReplyPolicy,
20788
20287
  recordSkippedSubscribedMessage: async ({
20789
20288
  thread,
@@ -21717,11 +21216,31 @@ function pluginConfigFromAgentPlugins(plugins) {
21717
21216
  ];
21718
21217
  return packages.length ? { packages } : void 0;
21719
21218
  }
21219
+ async function resolveAgentPluginBaseConfig(plugins) {
21220
+ if (plugins.length === 0) {
21221
+ return resolveVirtualPluginConfig();
21222
+ }
21223
+ return resolveBuildPluginConfig();
21224
+ }
21225
+ function mountAgentPluginRoutes(app, routes) {
21226
+ for (const route of routes) {
21227
+ const handler = (c) => route.handler(c.req.raw);
21228
+ const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
21229
+ const explicitMethods = methods.filter(
21230
+ (method) => method !== "ALL"
21231
+ );
21232
+ if (methods.includes("ALL")) {
21233
+ app.all(route.path, handler);
21234
+ } else if (explicitMethods.length > 0) {
21235
+ app.on(explicitMethods, route.path, handler);
21236
+ }
21237
+ }
21238
+ }
21720
21239
  async function createApp(options) {
21721
21240
  const configuredPlugins = options?.plugins;
21722
21241
  const agentPlugins2 = isJuniorPluginArray(configuredPlugins) ? configuredPlugins : [];
21723
21242
  const pluginConfig = isJuniorPluginArray(configuredPlugins) ? mergePluginConfig(
21724
- await resolveVirtualPluginConfig(),
21243
+ await resolveAgentPluginBaseConfig(configuredPlugins),
21725
21244
  pluginConfigFromAgentPlugins(configuredPlugins)
21726
21245
  ) : configuredPlugins ?? await resolveBuildPluginConfig();
21727
21246
  validateAgentPlugins(agentPlugins2);
@@ -21729,11 +21248,13 @@ async function createApp(options) {
21729
21248
  const previousPluginConfig = setPluginConfig(pluginConfig);
21730
21249
  const previousAgentPlugins = setAgentPlugins(agentPlugins2);
21731
21250
  const previousConfigDefaults = getConfigDefaults();
21251
+ let agentPluginRoutes = [];
21732
21252
  try {
21733
21253
  setConfigDefaults(options?.configDefaults);
21734
21254
  if (shouldValidatePluginCatalog) {
21735
21255
  getPluginCatalogSignature();
21736
21256
  }
21257
+ agentPluginRoutes = getAgentPluginRoutes();
21737
21258
  } catch (error) {
21738
21259
  setPluginConfig(previousPluginConfig);
21739
21260
  setAgentPlugins(previousAgentPlugins);
@@ -21752,6 +21273,7 @@ async function createApp(options) {
21752
21273
  }
21753
21274
  await next();
21754
21275
  });
21276
+ mountAgentPluginRoutes(app, agentPluginRoutes);
21755
21277
  app.get("/", () => GET());
21756
21278
  app.get("/health", () => GET());
21757
21279
  app.get("/api/oauth/callback/mcp/:provider", (c) => {