@sentry/junior 0.57.0 → 0.58.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.
Files changed (54) hide show
  1. package/dist/app.js +1301 -1278
  2. package/dist/chat/agent-dispatch/types.d.ts +0 -1
  3. package/dist/chat/conversation-privacy.d.ts +23 -0
  4. package/dist/chat/logging.d.ts +2 -0
  5. package/dist/chat/mcp/tool-manager.d.ts +18 -5
  6. package/dist/chat/mcp/tool-name.d.ts +2 -0
  7. package/dist/chat/pi/client.d.ts +2 -0
  8. package/dist/chat/pi/derived-state.d.ts +5 -0
  9. package/dist/chat/pi/traced-stream.d.ts +5 -1
  10. package/dist/chat/prompt.d.ts +3 -9
  11. package/dist/chat/respond-helpers.d.ts +5 -3
  12. package/dist/chat/respond.d.ts +1 -0
  13. package/dist/chat/runtime/conversation-message.d.ts +10 -0
  14. package/dist/chat/runtime/processing-reaction.d.ts +2 -4
  15. package/dist/chat/runtime/reply-executor.d.ts +13 -16
  16. package/dist/chat/runtime/slack-runtime.d.ts +19 -32
  17. package/dist/chat/runtime/thread-state.d.ts +1 -1
  18. package/dist/chat/runtime/turn-input.d.ts +29 -0
  19. package/dist/chat/runtime/turn-preparation.d.ts +4 -24
  20. package/dist/chat/runtime/turn.d.ts +2 -3
  21. package/dist/chat/sentry-links.d.ts +4 -0
  22. package/dist/chat/services/context-compaction.d.ts +3 -4
  23. package/dist/chat/services/pending-auth.d.ts +1 -1
  24. package/dist/chat/services/subscribed-reply-policy.d.ts +2 -13
  25. package/dist/chat/services/timeout-resume.d.ts +1 -2
  26. package/dist/chat/services/turn-session-record.d.ts +82 -0
  27. package/dist/chat/slack/assistant-thread/title.d.ts +4 -1
  28. package/dist/chat/state/artifacts.d.ts +1 -0
  29. package/dist/chat/state/conversation.d.ts +0 -1
  30. package/dist/chat/state/session-log.d.ts +117 -0
  31. package/dist/chat/state/ttl.d.ts +2 -0
  32. package/dist/chat/state/turn-session.d.ts +89 -0
  33. package/dist/chat/tools/advisor/tool.d.ts +2 -0
  34. package/dist/chat/tools/agent-tools.d.ts +2 -1
  35. package/dist/chat/tools/skill/call-mcp-tool.d.ts +7 -3
  36. package/dist/chat/tools/skill/search-mcp-tools.d.ts +15 -3
  37. package/dist/chat/tools/types.d.ts +0 -1
  38. package/dist/{chunk-AA5TIFN5.js → chunk-FKEKRBUB.js} +267 -735
  39. package/dist/{chunk-TTUY467K.js → chunk-H652GMDH.js} +30 -14
  40. package/dist/chunk-I4FDGMFI.js +950 -0
  41. package/dist/{chunk-D3G3YOU4.js → chunk-ITOW4DED.js} +1 -1
  42. package/dist/chunk-QDGD5WVN.js +708 -0
  43. package/dist/cli/check.js +2 -2
  44. package/dist/cli/init.js +0 -1
  45. package/dist/cli/snapshot-warmup.js +5 -3
  46. package/dist/instrumentation.js +3 -0
  47. package/dist/reporting.d.ts +113 -0
  48. package/dist/reporting.js +390 -0
  49. package/package.json +25 -11
  50. package/dist/chat/services/turn-checkpoint.d.ts +0 -74
  51. package/dist/chat/state/pi-session-message-store.d.ts +0 -15
  52. package/dist/chat/state/turn-session-store.d.ts +0 -49
  53. package/dist/handlers/diagnostics-dashboard.d.ts +0 -2
  54. package/dist/handlers/diagnostics.d.ts +0 -2
package/dist/app.js CHANGED
@@ -1,38 +1,66 @@
1
+ import {
2
+ GET,
3
+ abandonAgentTurnSessionRecord,
4
+ buildSentryConversationUrl,
5
+ commitMessages,
6
+ failAgentTurnSessionRecord,
7
+ getAgentTurnSessionRecord,
8
+ loadConnectedMcpProviders,
9
+ loadProjection,
10
+ recordAgentTurnSessionSummary,
11
+ recordAuthorizationCompleted,
12
+ recordAuthorizationRequested,
13
+ recordMcpProviderConnected,
14
+ upsertAgentTurnSessionRecord
15
+ } from "./chunk-I4FDGMFI.js";
1
16
  import {
2
17
  discoverSkills,
3
18
  findSkillByName,
4
19
  loadSkillsByName,
5
20
  parseSkillInvocation
6
- } from "./chunk-D3G3YOU4.js";
21
+ } from "./chunk-ITOW4DED.js";
7
22
  import {
8
- ACTIVE_LOCK_TTL_MS,
9
- GEN_AI_PROVIDER_NAME,
10
- MISSING_GATEWAY_CREDENTIALS_ERROR,
11
23
  SANDBOX_DATA_ROOT,
12
24
  SANDBOX_SKILLS_ROOT,
13
25
  SANDBOX_WORKSPACE_ROOT,
14
- botConfig,
15
26
  buildNonInteractiveShellScript,
27
+ createSandboxInstance,
28
+ getRuntimeDependencyProfileHash,
29
+ getVercelSandboxCredentials,
30
+ isSnapshotMissingError,
31
+ resolveRuntimeDependencySnapshot,
32
+ runNonInteractiveCommand,
33
+ sandboxSkillDir,
34
+ sandboxSkillFile
35
+ } from "./chunk-QDGD5WVN.js";
36
+ import {
37
+ ACTIVE_LOCK_TTL_MS,
38
+ GEN_AI_PROVIDER_NAME,
39
+ GEN_AI_SERVER_ADDRESS,
40
+ GEN_AI_SERVER_PORT,
41
+ MISSING_GATEWAY_CREDENTIALS_ERROR,
42
+ botConfig,
16
43
  completeObject,
17
44
  completeText,
18
- createSandboxInstance,
19
45
  getGatewayApiKey,
20
46
  getPiGatewayApiKeyOverride,
21
- getRuntimeDependencyProfileHash,
22
47
  getRuntimeMetadata,
23
48
  getSlackBotToken,
24
49
  getSlackClientId,
25
50
  getSlackClientSecret,
26
51
  getSlackSigningSecret,
27
52
  getStateAdapter,
28
- getVercelSandboxCredentials,
29
- isSnapshotMissingError,
53
+ parseSlackThreadId,
54
+ resolveConversationPrivacy,
30
55
  resolveGatewayModel,
31
- resolveRuntimeDependencySnapshot,
32
- runNonInteractiveCommand,
33
- sandboxSkillDir,
34
- sandboxSkillFile
35
- } from "./chunk-AA5TIFN5.js";
56
+ resolveSlackChannelIdFromMessage,
57
+ resolveSlackChannelIdFromThreadId,
58
+ toGenAiMessageMetadata,
59
+ toGenAiMessagesTraceAttributes,
60
+ toGenAiPayloadMetadata,
61
+ toGenAiPayloadTraceAttributes,
62
+ toGenAiTextMetadata
63
+ } from "./chunk-FKEKRBUB.js";
36
64
  import {
37
65
  CredentialUnavailableError,
38
66
  buildOAuthTokenRequest,
@@ -49,7 +77,6 @@ import {
49
77
  getPluginDefinition,
50
78
  getPluginMcpProviders,
51
79
  getPluginOAuthConfig,
52
- getPluginPackageContent,
53
80
  getPluginProviders,
54
81
  hasRequiredOAuthScope,
55
82
  isPluginConfigKey,
@@ -59,6 +86,7 @@ import {
59
86
  logException,
60
87
  logInfo,
61
88
  logWarn,
89
+ normalizeGenAiFinishReason,
62
90
  parseOAuthTokenResponse,
63
91
  resolveAuthTokenPlaceholder,
64
92
  resolvePluginCommandEnv,
@@ -71,7 +99,7 @@ import {
71
99
  toOptionalString,
72
100
  withContext,
73
101
  withSpan
74
- } from "./chunk-TTUY467K.js";
102
+ } from "./chunk-H652GMDH.js";
75
103
  import {
76
104
  sentry_exports
77
105
  } from "./chunk-Z3YD6NHK.js";
@@ -468,252 +496,13 @@ function createAgentPluginHookRunner(input = {}) {
468
496
  };
469
497
  }
470
498
 
471
- // src/handlers/diagnostics.ts
472
- import { readFileSync } from "fs";
473
- import path from "path";
474
- function readDescriptionText() {
475
- try {
476
- const raw = readFileSync(
477
- path.join(homeDir(), "DESCRIPTION.md"),
478
- "utf8"
479
- ).trim();
480
- return raw || void 0;
481
- } catch {
482
- return void 0;
483
- }
484
- }
485
- async function GET() {
486
- const packagedContent = getPluginPackageContent();
487
- const skills = await discoverSkills();
488
- return Response.json({
489
- cwd: process.cwd(),
490
- homeDir: homeDir(),
491
- descriptionText: readDescriptionText(),
492
- providers: getPluginProviders().map((plugin) => plugin.manifest.name),
493
- skills: skills.map((skill) => ({
494
- name: skill.name,
495
- pluginProvider: skill.pluginProvider
496
- })),
497
- packagedContent
498
- });
499
- }
500
-
501
- // src/chat/xml.ts
502
- function escapeXml(value) {
503
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
504
- }
505
-
506
- // src/handlers/health.ts
507
- function GET2() {
508
- return Response.json({
509
- status: "ok",
510
- service: "junior",
511
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
512
- });
513
- }
514
-
515
- // src/handlers/diagnostics-dashboard.ts
516
- async function GET3() {
517
- let health;
518
- let discovery;
519
- try {
520
- const res = await GET2();
521
- health = {
522
- ok: res.ok,
523
- data: await res.json()
524
- };
525
- } catch (e) {
526
- health = { ok: false, error: String(e) };
527
- }
528
- try {
529
- const res = await GET();
530
- if (res.ok) {
531
- discovery = {
532
- ok: true,
533
- data: await res.json()
534
- };
535
- } else {
536
- discovery = { ok: false, error: `${res.status} ${res.statusText}` };
537
- }
538
- } catch (e) {
539
- discovery = { ok: false, error: String(e) };
540
- }
541
- const d = discovery.ok ? discovery.data : null;
542
- let html = `<!DOCTYPE html>
543
- <html lang="en">
544
- <head>
545
- <meta charset="utf-8" />
546
- <meta name="viewport" content="width=device-width, initial-scale=1" />
547
- <title>Junior</title>
548
- <style>
549
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
550
- body {
551
- font-family: "SF Mono", "Cascadia Code", "Fira Code", Menlo, monospace;
552
- background: #0d1117; color: #c9d1d9; padding: 2rem;
553
- font-size: 14px; line-height: 1.6;
554
- }
555
- h1 { color: #58a6ff; font-size: 1.1rem; margin-bottom: 0.25rem; }
556
- .subtitle { color: #8b949e; font-size: 0.85rem; margin-bottom: 1.5rem; }
557
- .section { max-width: 720px; margin-bottom: 1.25rem; }
558
- .section-title {
559
- color: #8b949e; font-size: 0.75rem; text-transform: uppercase;
560
- letter-spacing: 0.08em; margin-bottom: 0.5rem; padding-bottom: 0.25rem;
561
- border-bottom: 1px solid #21262d;
562
- }
563
- .status-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
564
- .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
565
- .dot-ok { background: #2dd4bf; }
566
- .dot-err { background: #f87171; }
567
- .label { color: #8b949e; }
568
- .value { color: #e6edf3; }
569
- .detail-row { display: flex; gap: 0.5rem; margin-bottom: 0.25rem; font-size: 0.85rem; }
570
- .detail-key { color: #8b949e; min-width: 7rem; }
571
- .detail-val { color: #c9d1d9; }
572
- .skill-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; }
573
- .skill-tag {
574
- background: #161b22; border: 1px solid #30363d; border-radius: 4px;
575
- padding: 0.2rem 0.5rem; font-size: 0.8rem; color: #c9d1d9;
576
- }
577
- .skill-provider { color: #8b949e; font-size: 0.7rem; margin-left: 0.15rem; }
578
- .provider-list, .package-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
579
- .provider-tag {
580
- background: #1b2332; border: 1px solid #1f3a5f; border-radius: 4px;
581
- padding: 0.2rem 0.5rem; font-size: 0.8rem; color: #58a6ff;
582
- }
583
- .package-tag {
584
- background: #1a1e2a; border: 1px solid #2d3548; border-radius: 4px;
585
- padding: 0.2rem 0.5rem; font-size: 0.78rem; color: #a5b4cf;
586
- }
587
- .endpoint-list { list-style: none; }
588
- .endpoint-list li { margin-bottom: 0.2rem; font-size: 0.85rem; }
589
- .method {
590
- display: inline-block; font-size: 0.7rem; font-weight: 600;
591
- padding: 0.1rem 0.35rem; border-radius: 3px; margin-right: 0.4rem;
592
- min-width: 2.5rem; text-align: center;
593
- }
594
- .method-get { background: #1b3a2d; color: #2dd4bf; }
595
- .method-post { background: #3b2e1a; color: #f0b952; }
596
- .endpoint-link { color: #c9d1d9; text-decoration: none; }
597
- .endpoint-link:hover { color: #58a6ff; text-decoration: underline; }
598
- .error-msg { color: #f87171; font-size: 0.85rem; }
599
- </style>
600
- </head>
601
- <body>
602
- <h1>&gt; junior</h1>`;
603
- if (d?.descriptionText) {
604
- html += `
605
- <div class="subtitle">${escapeXml(String(d.descriptionText))}</div>`;
606
- }
607
- html += `
608
- <div class="section">
609
- <div class="section-title">Status</div>
610
- <div class="status-row">
611
- <span class="dot ${health.ok ? "dot-ok" : "dot-err"}"></span>
612
- <span class="value">${health.ok ? "Healthy" : "Unreachable"}</span>`;
613
- if (health.ok && health.data?.timestamp) {
614
- html += `
615
- <span class="label">&middot; ${escapeXml(new Date(health.data.timestamp).toLocaleTimeString())}</span>`;
616
- }
617
- html += `
618
- </div>`;
619
- if (d) {
620
- html += `
621
- <div class="detail-row"><span class="detail-key">service</span><span class="detail-val">${escapeXml(String(health.data?.service ?? "junior"))}</span></div>`;
622
- html += `
623
- <div class="detail-row"><span class="detail-key">cwd</span><span class="detail-val">${escapeXml(String(d.cwd))}</span></div>`;
624
- html += `
625
- <div class="detail-row"><span class="detail-key">home</span><span class="detail-val">${escapeXml(String(d.homeDir))}</span></div>`;
626
- }
627
- html += `
628
- </div>`;
629
- const endpoints = [
630
- { method: "GET", path: "/health" },
631
- { method: "GET", path: "/api/info" },
632
- { method: "GET", path: "/api/oauth/callback/mcp/:provider" },
633
- { method: "GET", path: "/api/oauth/callback/:provider" },
634
- { method: "POST", path: "/api/internal/agent-dispatch" },
635
- { method: "GET", path: "/api/internal/heartbeat" },
636
- { method: "POST", path: "/api/webhooks/:platform" }
637
- ];
638
- html += `
639
- <div class="section">
640
- <div class="section-title">Endpoints</div>
641
- <ul class="endpoint-list">`;
642
- for (const ep of endpoints) {
643
- const cls = ep.method === "GET" ? "method-get" : "method-post";
644
- const link = ep.path.includes(":") ? `<span>${escapeXml(ep.path)}</span>` : `<a class="endpoint-link" href="${escapeXml(ep.path)}" target="_blank">${escapeXml(ep.path)}</a>`;
645
- html += `
646
- <li><span class="method ${cls}">${escapeXml(ep.method)}</span>${link}</li>`;
647
- }
648
- html += `
649
- </ul>
650
- </div>`;
651
- if (d) {
652
- const providers = d.providers;
653
- const packagedContent = d.packagedContent;
654
- const skills = d.skills;
655
- if (providers?.length) {
656
- html += `
657
- <div class="section">
658
- <div class="section-title">Plugins <span class="label">(${providers.length})</span></div>
659
- <div class="provider-list">`;
660
- for (const p of providers) {
661
- html += `
662
- <span class="provider-tag">${escapeXml(p)}</span>`;
663
- }
664
- html += `
665
- </div>`;
666
- if (packagedContent?.packageNames?.length) {
667
- html += `
668
- <div style="margin-top:0.5rem"><div class="package-list">`;
669
- for (const pkg of packagedContent.packageNames) {
670
- html += `
671
- <span class="package-tag">${escapeXml(pkg)}</span>`;
672
- }
673
- html += `
674
- </div></div>`;
675
- }
676
- html += `
677
- </div>`;
678
- }
679
- if (skills?.length) {
680
- html += `
681
- <div class="section">
682
- <div class="section-title">Skills <span class="label">(${skills.length})</span></div>
683
- <div class="skill-grid">`;
684
- for (const s of skills) {
685
- html += `
686
- <span class="skill-tag">${escapeXml(s.name)}`;
687
- if (s.pluginProvider) {
688
- html += ` <span class="skill-provider">${escapeXml(s.pluginProvider)}</span>`;
689
- }
690
- html += `</span>`;
691
- }
692
- html += `
693
- </div>
694
- </div>`;
695
- }
696
- } else if (!discovery.ok) {
697
- html += `
698
- <div class="section">
699
- <div class="section-title">Discovery</div>
700
- <span class="error-msg">unavailable &middot; ${escapeXml(discovery.error ?? "unknown")}</span>
701
- </div>`;
702
- }
703
- html += `
704
- </body>
705
- </html>`;
706
- return new Response(html, {
707
- headers: { "content-type": "text/html; charset=utf-8" }
708
- });
709
- }
710
-
711
499
  // src/chat/respond.ts
712
500
  import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
501
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
713
502
 
714
503
  // src/chat/prompt.ts
715
504
  import fs from "fs";
716
- import path2 from "path";
505
+ import path from "path";
717
506
 
718
507
  // src/chat/turn-context-tag.ts
719
508
  var TURN_CONTEXT_TAG = "runtime-turn-context";
@@ -999,6 +788,11 @@ var slackOutputPolicy = {
999
788
  maxInlineLines: MAX_INLINE_LINES
1000
789
  };
1001
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
+
1002
796
  // src/chat/prompt.ts
1003
797
  var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1004
798
  function getLoggedMarkdownFiles() {
@@ -1127,7 +921,7 @@ function formatAvailableSkillsForPrompt(skills, invocation) {
1127
921
  if (autoSelectable.length > 0) {
1128
922
  const available = [
1129
923
  "<available-skills>",
1130
- "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."
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."
1131
925
  ];
1132
926
  for (const skill of autoSelectable) {
1133
927
  available.push(...formatSkillEntry(skill));
@@ -1148,26 +942,6 @@ function formatAvailableSkillsForPrompt(skills, invocation) {
1148
942
  }
1149
943
  return sections.length > 0 ? sections.join("\n") : null;
1150
944
  }
1151
- function formatLoadedSkillsForPrompt(skills) {
1152
- if (skills.length === 0) {
1153
- return null;
1154
- }
1155
- const lines = ["<loaded-skills>"];
1156
- for (const skill of skills) {
1157
- const skillDir = workspaceSkillDir(skill.name);
1158
- lines.push(
1159
- ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
1160
- );
1161
- lines.push(
1162
- `Skill directory: ${escapeXml(skillDir)}. Resolve relative paths there; for skill-owned bash commands, cd there first or use absolute paths.`
1163
- );
1164
- lines.push("");
1165
- lines.push(skill.body);
1166
- lines.push(" </skill>");
1167
- }
1168
- lines.push("</loaded-skills>");
1169
- return lines.join("\n");
1170
- }
1171
945
  function formatActiveMcpCatalogsForPrompt(catalogs) {
1172
946
  if (catalogs.length === 0) {
1173
947
  return null;
@@ -1213,7 +987,7 @@ function formatReferenceFilesLines() {
1213
987
  return null;
1214
988
  }
1215
989
  return files.map((filePath) => {
1216
- const name = path2.basename(filePath);
990
+ const name = path.basename(filePath);
1217
991
  return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1218
992
  });
1219
993
  }
@@ -1255,7 +1029,7 @@ function formatConfigurationLines(configuration) {
1255
1029
  );
1256
1030
  }
1257
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.";
1258
- var TURN_CONTEXT_HEADER = "Per-turn runtime context for this request. Treat these blocks as trusted runtime facts and skill/provider instructions for the current turn; the static system prompt remains authoritative.";
1032
+ var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1259
1033
  var TOOL_POLICY_RULES = [
1260
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.",
1261
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.",
@@ -1275,7 +1049,7 @@ var TOOL_CALL_STYLE_RULES = [
1275
1049
  ];
1276
1050
  var SKILL_POLICY_RULES = [
1277
1051
  "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1278
- "- Load one skill at a time. After `loadSkill`, follow the instructions in `<loaded-skills>`."
1052
+ "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1279
1053
  ];
1280
1054
  var EXECUTION_CONTRACT_RULES = [
1281
1055
  "- Actionable request: act in this turn.",
@@ -1386,12 +1160,6 @@ function buildContextSection(params) {
1386
1160
  ])
1387
1161
  );
1388
1162
  }
1389
- if (params.turnState === "resumed") {
1390
- blocks.push([
1391
- "<turn-state>resumed</turn-state>",
1392
- "This turn continues from a prior checkpoint. Prior tool results and assistant messages are already in the conversation history."
1393
- ]);
1394
- }
1395
1163
  if (params.invocation) {
1396
1164
  blocks.push(
1397
1165
  renderTag("explicit-skill-trigger", [
@@ -1415,10 +1183,6 @@ function buildCapabilitiesSection(params) {
1415
1183
  if (availableSkills) {
1416
1184
  blocks.push(availableSkills);
1417
1185
  }
1418
- const loadedSkills = formatLoadedSkillsForPrompt(params.activeSkills);
1419
- if (loadedSkills) {
1420
- blocks.push(loadedSkills);
1421
- }
1422
1186
  const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1423
1187
  params.activeMcpCatalogs
1424
1188
  );
@@ -1445,13 +1209,13 @@ function buildSystemPrompt() {
1445
1209
  return STATIC_SYSTEM_PROMPT;
1446
1210
  }
1447
1211
  function buildTurnContextPrompt(params) {
1448
- const sections = [
1449
- `<${TURN_CONTEXT_TAG}>`,
1450
- TURN_CONTEXT_HEADER,
1451
- params.turnState === "resumed" ? "Continue the pending turn from prior conversation history; this block is not a new user request." : "The current user instruction appears after this block in the same message.",
1212
+ const includeSessionContext = params.includeSessionContext ?? true;
1213
+ if (!includeSessionContext) {
1214
+ return null;
1215
+ }
1216
+ const runtimeSections = [
1452
1217
  buildCapabilitiesSection({
1453
1218
  availableSkills: params.availableSkills,
1454
- activeSkills: params.activeSkills,
1455
1219
  activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1456
1220
  invocation: params.invocation,
1457
1221
  toolGuidance: params.toolGuidance ?? []
@@ -1460,10 +1224,18 @@ function buildTurnContextPrompt(params) {
1460
1224
  requester: params.requester,
1461
1225
  artifactState: params.artifactState,
1462
1226
  configuration: params.configuration,
1463
- invocation: params.invocation,
1464
- turnState: params.turnState
1227
+ invocation: params.invocation
1465
1228
  }),
1466
- buildRuntimeSection(params.runtime ?? {}),
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,
1467
1239
  `</${TURN_CONTEXT_TAG}>`
1468
1240
  ].filter((section) => Boolean(section));
1469
1241
  return sections.join("\n\n");
@@ -1864,12 +1636,12 @@ async function maybeExecuteJrRpcCustomCommand(command, deps) {
1864
1636
 
1865
1637
  // src/chat/sandbox/skill-sandbox.ts
1866
1638
  import fs2 from "fs/promises";
1867
- import path3 from "path";
1639
+ import path2 from "path";
1868
1640
  var MAX_SKILL_FILE_BYTES = 256 * 1024;
1869
1641
  var DEFAULT_MAX_SKILL_FILE_CHARS = 2e4;
1870
1642
  var DEFAULT_MAX_SKILL_LIST_ENTRIES = 200;
1871
1643
  function normalizePathForOutput(value) {
1872
- return value.split(path3.sep).join("/");
1644
+ return value.split(path2.sep).join("/");
1873
1645
  }
1874
1646
  function normalizeSkillName(value) {
1875
1647
  return value.trim().toLowerCase();
@@ -1878,12 +1650,12 @@ function resolvePathWithinRoot(root, relativePath) {
1878
1650
  if (!relativePath.trim()) {
1879
1651
  throw new Error("Path must not be empty.");
1880
1652
  }
1881
- if (path3.isAbsolute(relativePath)) {
1653
+ if (path2.isAbsolute(relativePath)) {
1882
1654
  throw new Error("Absolute paths are not allowed.");
1883
1655
  }
1884
- const resolvedRoot = path3.resolve(root);
1885
- const resolvedPath = path3.resolve(resolvedRoot, relativePath);
1886
- if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path3.sep}`)) {
1656
+ const resolvedRoot = path2.resolve(root);
1657
+ const resolvedPath = path2.resolve(resolvedRoot, relativePath);
1658
+ if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path2.sep}`)) {
1887
1659
  throw new Error("Path escapes the skill directory.");
1888
1660
  }
1889
1661
  return resolvedPath;
@@ -1963,7 +1735,7 @@ var SkillSandbox = class {
1963
1735
  1,
1964
1736
  Math.min(params.maxEntries ?? DEFAULT_MAX_SKILL_LIST_ENTRIES, 1e3)
1965
1737
  );
1966
- const root = path3.resolve(skill.skillPath);
1738
+ const root = path2.resolve(skill.skillPath);
1967
1739
  const targetDirectory = resolvePathWithinRoot(root, directory);
1968
1740
  const targetStats = await fs2.stat(targetDirectory);
1969
1741
  if (!targetStats.isDirectory()) {
@@ -1979,9 +1751,9 @@ var SkillSandbox = class {
1979
1751
  });
1980
1752
  children.sort((a, b) => a.name.localeCompare(b.name));
1981
1753
  for (const child of children) {
1982
- const absolutePath = path3.join(currentDirectory, child.name);
1754
+ const absolutePath = path2.join(currentDirectory, child.name);
1983
1755
  const relativePath = normalizePathForOutput(
1984
- path3.relative(root, absolutePath)
1756
+ path2.relative(root, absolutePath)
1985
1757
  );
1986
1758
  if (!relativePath || relativePath.startsWith("..")) {
1987
1759
  continue;
@@ -2004,7 +1776,7 @@ var SkillSandbox = class {
2004
1776
  }
2005
1777
  }
2006
1778
  const relativeDirectory = normalizePathForOutput(
2007
- path3.relative(root, targetDirectory) || "."
1779
+ path2.relative(root, targetDirectory) || "."
2008
1780
  );
2009
1781
  return {
2010
1782
  skillName: skill.name,
@@ -2019,7 +1791,7 @@ var SkillSandbox = class {
2019
1791
  1,
2020
1792
  Math.min(params.maxChars ?? DEFAULT_MAX_SKILL_FILE_CHARS, 1e5)
2021
1793
  );
2022
- const root = path3.resolve(skill.skillPath);
1794
+ const root = path2.resolve(skill.skillPath);
2023
1795
  const targetPath = resolvePathWithinRoot(root, params.filePath);
2024
1796
  const stats = await fs2.stat(targetPath);
2025
1797
  if (!stats.isFile()) {
@@ -2034,7 +1806,7 @@ var SkillSandbox = class {
2034
1806
  const truncated = raw.length > maxChars;
2035
1807
  return {
2036
1808
  skillName: skill.name,
2037
- path: normalizePathForOutput(path3.relative(root, targetPath)),
1809
+ path: normalizePathForOutput(path2.relative(root, targetPath)),
2038
1810
  content: truncated ? raw.slice(0, maxChars) : raw,
2039
1811
  truncated
2040
1812
  };
@@ -2402,6 +2174,14 @@ var McpToolManager = class {
2402
2174
  (left, right) => left.localeCompare(right)
2403
2175
  );
2404
2176
  }
2177
+ /** List configured MCP providers for discovery without connecting to them. */
2178
+ getAvailableProviderCatalog() {
2179
+ return [...this.pluginsByProvider.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([provider, plugin]) => ({
2180
+ provider,
2181
+ description: plugin.manifest.description,
2182
+ active: this.activeProviders.has(provider)
2183
+ }));
2184
+ }
2405
2185
  async activateForSkill(skill) {
2406
2186
  if (!skill.pluginProvider) {
2407
2187
  return false;
@@ -2452,8 +2232,9 @@ var McpToolManager = class {
2452
2232
  throw firstError;
2453
2233
  }
2454
2234
  }
2455
- getActiveToolCatalog(skills, options = {}) {
2456
- return this.getResolvedActiveTools(skills, options).map(
2235
+ /** Return descriptors for all active MCP provider tools, optionally filtered by provider. */
2236
+ getActiveToolCatalog(options = {}) {
2237
+ return this.getResolvedActiveTools(options).map(
2457
2238
  (tool2) => this.toToolDescriptor(tool2)
2458
2239
  );
2459
2240
  }
@@ -2573,30 +2354,17 @@ var McpToolManager = class {
2573
2354
  this.activeProviders.delete(provider);
2574
2355
  return true;
2575
2356
  }
2576
- /** Return all active ManagedMcpTool objects for the given skill scope. */
2577
- getResolvedActiveTools(skills, options = {}) {
2357
+ /** Return all active ManagedMcpTool objects, optionally filtered by provider. */
2358
+ getResolvedActiveTools(options = {}) {
2578
2359
  const resolved = [];
2579
2360
  for (const provider of this.getActiveProviders()) {
2580
2361
  if (options.provider && provider !== options.provider) {
2581
2362
  continue;
2582
2363
  }
2583
- resolved.push(...this.resolveProviderTools(provider, skills));
2364
+ resolved.push(...this.toolsByProvider.get(provider) ?? []);
2584
2365
  }
2585
2366
  return resolved;
2586
2367
  }
2587
- resolveProviderTools(provider, skills) {
2588
- const providerTools = this.toolsByProvider.get(provider) ?? [];
2589
- if (providerTools.length === 0) {
2590
- return [];
2591
- }
2592
- const relevantSkills = skills.filter(
2593
- (skill) => skill.pluginProvider === provider
2594
- );
2595
- if (relevantSkills.length === 0) {
2596
- return [];
2597
- }
2598
- return providerTools;
2599
- }
2600
2368
  toToolDescriptor(tool2) {
2601
2369
  return {
2602
2370
  name: tool2.name,
@@ -2614,6 +2382,136 @@ function toOptionalRecord(value) {
2614
2382
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2615
2383
  }
2616
2384
 
2385
+ // src/chat/mcp/tool-name.ts
2386
+ function parseMcpProviderFromToolName(toolName) {
2387
+ if (!toolName.startsWith("mcp__")) return void 0;
2388
+ const afterPrefix = toolName.slice("mcp__".length);
2389
+ const delimiterIndex = afterPrefix.indexOf("__");
2390
+ return delimiterIndex > 0 ? afterPrefix.slice(0, delimiterIndex) : void 0;
2391
+ }
2392
+
2393
+ // src/chat/pi/derived-state.ts
2394
+ var MCP_BRIDGE_TOOLS = /* @__PURE__ */ new Set(["callMcpTool", "searchMcpTools"]);
2395
+ function isRecord3(value) {
2396
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2397
+ }
2398
+ function providerFromToolName(value) {
2399
+ if (typeof value !== "string") {
2400
+ return void 0;
2401
+ }
2402
+ return parseMcpProviderFromToolName(value);
2403
+ }
2404
+ function addString(values, value) {
2405
+ if (typeof value === "string" && value.trim()) {
2406
+ values.add(value.trim());
2407
+ }
2408
+ }
2409
+ function getToolName(value) {
2410
+ return typeof value.toolName === "string" ? value.toolName : typeof value.name === "string" ? value.name : void 0;
2411
+ }
2412
+ function addBridgeToolProvider(toolName, value, providers) {
2413
+ const bridgeTool = toolName && MCP_BRIDGE_TOOLS.has(toolName) ? toolName : void 0;
2414
+ if (bridgeTool === "searchMcpTools") {
2415
+ for (const argsKey of ["input", "args", "arguments", "params"]) {
2416
+ const args = value[argsKey];
2417
+ if (isRecord3(args)) {
2418
+ addString(providers, args.provider);
2419
+ }
2420
+ }
2421
+ addString(providers, value.provider);
2422
+ }
2423
+ if (bridgeTool === "callMcpTool") {
2424
+ for (const argsKey of ["input", "args", "arguments", "params"]) {
2425
+ const args = value[argsKey];
2426
+ if (isRecord3(args)) {
2427
+ addString(providers, providerFromToolName(args.tool_name));
2428
+ }
2429
+ }
2430
+ addString(providers, providerFromToolName(value.tool_name));
2431
+ }
2432
+ }
2433
+ function addMcpResultProvider(message, providers) {
2434
+ const toolName = typeof message.toolName === "string" ? message.toolName : void 0;
2435
+ if (message.isError === true) {
2436
+ return;
2437
+ }
2438
+ if (toolName === "loadSkill") {
2439
+ if (isRecord3(message.details)) {
2440
+ addString(providers, message.details.mcp_provider);
2441
+ }
2442
+ addString(providers, message.mcp_provider);
2443
+ return;
2444
+ }
2445
+ if (toolName === "searchMcpTools") {
2446
+ if (isRecord3(message.details)) {
2447
+ addString(providers, message.details.provider);
2448
+ if (Array.isArray(message.details.tools)) {
2449
+ for (const tool2 of message.details.tools) {
2450
+ if (isRecord3(tool2)) {
2451
+ addString(providers, providerFromToolName(tool2.tool_name));
2452
+ }
2453
+ }
2454
+ }
2455
+ }
2456
+ addString(providers, message.provider);
2457
+ return;
2458
+ }
2459
+ if (toolName === "callMcpTool") {
2460
+ for (const argsKey of ["input", "args", "arguments", "params"]) {
2461
+ const args = message[argsKey];
2462
+ if (isRecord3(args)) {
2463
+ addString(providers, providerFromToolName(args.tool_name));
2464
+ }
2465
+ }
2466
+ if (isRecord3(message.details)) {
2467
+ addString(providers, message.details.provider);
2468
+ addString(providers, providerFromToolName(message.details.tool_name));
2469
+ }
2470
+ addString(providers, providerFromToolName(message.tool_name));
2471
+ }
2472
+ }
2473
+ function scanMcpProviders(message, providers) {
2474
+ if (!isRecord3(message)) {
2475
+ return;
2476
+ }
2477
+ if (message.role === "toolResult") {
2478
+ addMcpResultProvider(message, providers);
2479
+ return;
2480
+ }
2481
+ const content = message.content;
2482
+ if (!Array.isArray(content)) {
2483
+ return;
2484
+ }
2485
+ for (const part of content) {
2486
+ if (!isRecord3(part)) {
2487
+ continue;
2488
+ }
2489
+ addBridgeToolProvider(getToolName(part), part, providers);
2490
+ }
2491
+ }
2492
+ function scanLoadedSkills(message, skills) {
2493
+ if (isRecord3(message) && message.role === "toolResult" && message.toolName === "loadSkill" && message.isError !== true) {
2494
+ if (isRecord3(message.details)) {
2495
+ addString(skills, message.details.skill_name);
2496
+ }
2497
+ addString(skills, message.skill_name);
2498
+ }
2499
+ }
2500
+ function inferActiveMcpProvidersFromPiMessages(messages) {
2501
+ const providers = /* @__PURE__ */ new Set();
2502
+ for (const message of messages ?? []) {
2503
+ scanMcpProviders(message, providers);
2504
+ }
2505
+ return [...providers].sort((left, right) => left.localeCompare(right));
2506
+ }
2507
+ function inferLoadedSkillNamesFromPiMessages(messages) {
2508
+ const skills = /* @__PURE__ */ new Set();
2509
+ for (const message of messages ?? []) {
2510
+ scanLoadedSkills(message, skills);
2511
+ }
2512
+ return [...skills].sort((left, right) => left.localeCompare(right));
2513
+ }
2514
+
2617
2515
  // src/chat/tools/definition.ts
2618
2516
  function tool(definition) {
2619
2517
  return definition;
@@ -2646,7 +2544,7 @@ function createBashTool() {
2646
2544
  }
2647
2545
 
2648
2546
  // src/chat/tools/sandbox/file-utils.ts
2649
- import path4 from "path";
2547
+ import path3 from "path";
2650
2548
 
2651
2549
  // src/chat/tools/execution/tool-input-error.ts
2652
2550
  var ToolInputError = class extends Error {
@@ -2742,12 +2640,12 @@ function matchesGlob(relativePath, pattern) {
2742
2640
  if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) {
2743
2641
  return true;
2744
2642
  }
2745
- return !pattern.includes("/") && matcher.test(path4.posix.basename(relativePath));
2643
+ return !pattern.includes("/") && matcher.test(path3.posix.basename(relativePath));
2746
2644
  }
2747
2645
  function resolveWorkspacePath(input, fallback = ".") {
2748
2646
  const requested = (input ?? "").trim() || fallback;
2749
- const absolute = requested.startsWith("/") ? requested : path4.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
2750
- const normalized = path4.posix.normalize(absolute);
2647
+ const absolute = requested.startsWith("/") ? requested : path3.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
2648
+ const normalized = path3.posix.normalize(absolute);
2751
2649
  if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
2752
2650
  throw new ToolInputError(
2753
2651
  `Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
@@ -2776,7 +2674,7 @@ async function collectFiles(params) {
2776
2674
  throw error;
2777
2675
  }
2778
2676
  for (const entry of entries) {
2779
- const fullPath = path4.posix.join(dirPath, entry);
2677
+ const fullPath = path3.posix.join(dirPath, entry);
2780
2678
  let stat2;
2781
2679
  try {
2782
2680
  stat2 = await params.fs.stat(fullPath);
@@ -2794,7 +2692,7 @@ async function collectFiles(params) {
2794
2692
  if (limitReached) return;
2795
2693
  continue;
2796
2694
  }
2797
- const relativePath = path4.posix.relative(params.root, fullPath);
2695
+ const relativePath = path3.posix.relative(params.root, fullPath);
2798
2696
  if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
2799
2697
  files.push(fullPath);
2800
2698
  if (params.limit && files.length >= params.limit) {
@@ -2819,7 +2717,7 @@ async function collectFiles(params) {
2819
2717
  throw error;
2820
2718
  }
2821
2719
  if (!stat.isDirectory()) {
2822
- const relativePath = path4.posix.basename(params.root);
2720
+ const relativePath = path3.posix.basename(params.root);
2823
2721
  return {
2824
2722
  files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
2825
2723
  limitReached: false,
@@ -3102,7 +3000,7 @@ function createEditFileTool() {
3102
3000
  }
3103
3001
 
3104
3002
  // src/chat/tools/sandbox/find-files.ts
3105
- import path5 from "path";
3003
+ import path4 from "path";
3106
3004
  import { Type as Type3 } from "@sinclair/typebox";
3107
3005
  var DEFAULT_FIND_LIMIT = 1e3;
3108
3006
  async function findFiles(params) {
@@ -3124,7 +3022,7 @@ async function findFiles(params) {
3124
3022
  });
3125
3023
  }
3126
3024
  const relativePaths = files.map(
3127
- (filePath) => path5.posix.relative(root, filePath)
3025
+ (filePath) => path4.posix.relative(root, filePath)
3128
3026
  );
3129
3027
  const bounded = truncateText(
3130
3028
  relativePaths.length > 0 ? relativePaths.join("\n") : "No files found matching pattern"
@@ -3189,7 +3087,7 @@ function createFindFilesTool() {
3189
3087
  }
3190
3088
 
3191
3089
  // src/chat/tools/sandbox/grep.ts
3192
- import path6 from "path";
3090
+ import path5 from "path";
3193
3091
  import { Type as Type4 } from "@sinclair/typebox";
3194
3092
  var DEFAULT_GREP_LIMIT = 100;
3195
3093
  var MAX_GREP_LINE_CHARS = 500;
@@ -3261,7 +3159,7 @@ async function grepFiles(params) {
3261
3159
  continue;
3262
3160
  }
3263
3161
  const lines = normalizeToLf(content).split("\n");
3264
- const relativePath = files.length === 1 && filePath === root ? path6.posix.basename(filePath) : path6.posix.relative(root, filePath);
3162
+ const relativePath = files.length === 1 && filePath === root ? path5.posix.basename(filePath) : path5.posix.relative(root, filePath);
3265
3163
  const matchedLines = [];
3266
3164
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3267
3165
  if (!lineMatches({
@@ -3388,7 +3286,7 @@ function createGrepTool() {
3388
3286
  }
3389
3287
 
3390
3288
  // src/chat/tools/sandbox/attach-file.ts
3391
- import path7 from "path";
3289
+ import path6 from "path";
3392
3290
  import { Type as Type5 } from "@sinclair/typebox";
3393
3291
  var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
3394
3292
  var MIME_BY_EXTENSION = {
@@ -3410,20 +3308,20 @@ function normalizeSandboxPath(inputPath) {
3410
3308
  if (!trimmed) {
3411
3309
  throw new Error("path is required");
3412
3310
  }
3413
- if (path7.posix.isAbsolute(trimmed)) {
3311
+ if (path6.posix.isAbsolute(trimmed)) {
3414
3312
  return trimmed;
3415
3313
  }
3416
- return path7.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
3314
+ return path6.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
3417
3315
  }
3418
3316
  function sanitizeFilename(value, fallbackPath) {
3419
3317
  const candidate = (value ?? "").trim();
3420
3318
  if (candidate) {
3421
- const base = path7.posix.basename(candidate);
3319
+ const base = path6.posix.basename(candidate);
3422
3320
  if (base && base !== "." && base !== "..") {
3423
3321
  return base;
3424
3322
  }
3425
3323
  }
3426
- const derived = path7.posix.basename(fallbackPath);
3324
+ const derived = path6.posix.basename(fallbackPath);
3427
3325
  if (derived && derived !== "." && derived !== "..") {
3428
3326
  return derived;
3429
3327
  }
@@ -3434,7 +3332,7 @@ function inferMimeType(filename, explicitMimeType) {
3434
3332
  if (explicit) {
3435
3333
  return explicit;
3436
3334
  }
3437
- const ext = path7.extname(filename).toLowerCase();
3335
+ const ext = path6.extname(filename).toLowerCase();
3438
3336
  return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
3439
3337
  }
3440
3338
  async function detectMimeType(sandbox, targetPath) {
@@ -3481,7 +3379,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
3481
3379
  const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
3482
3380
  if (!fileBuffer) {
3483
3381
  const generatedFile = hooks.getGeneratedFile?.(
3484
- path7.posix.basename(targetPath)
3382
+ path6.posix.basename(targetPath)
3485
3383
  );
3486
3384
  if (generatedFile) {
3487
3385
  hooks.onGeneratedFiles?.([generatedFile]);
@@ -3529,7 +3427,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
3529
3427
  }
3530
3428
 
3531
3429
  // src/chat/tools/sandbox/list-dir.ts
3532
- import path8 from "path";
3430
+ import path7 from "path";
3533
3431
  import { Type as Type6 } from "@sinclair/typebox";
3534
3432
  var DEFAULT_LIST_LIMIT = 500;
3535
3433
  async function listDir(params) {
@@ -3565,7 +3463,7 @@ async function listDir(params) {
3565
3463
  entryLimitReached = true;
3566
3464
  break;
3567
3465
  }
3568
- const entryPath = path8.posix.join(dirPath, entry);
3466
+ const entryPath = path7.posix.join(dirPath, entry);
3569
3467
  try {
3570
3468
  const entryStat = await params.fs.stat(entryPath);
3571
3469
  output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
@@ -3805,9 +3703,9 @@ function resolveMcpArguments(input) {
3805
3703
  }
3806
3704
  return {};
3807
3705
  }
3808
- function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
3706
+ function createCallMcpToolTool(mcpToolManager) {
3809
3707
  return tool({
3810
- description: "Call an active MCP tool by exact tool_name. Use loadSkill to activate the provider, then searchMcpTools to discover tool names and schemas; copy required provider fields into arguments. Do not call with only tool_name unless the discovered tool has no arguments. Authorization is handled by the runtime when required.",
3708
+ description: "Call an active MCP tool by exact tool_name. Use searchMcpTools to discover tool names and schemas; copy required provider fields into arguments. Do not call with only tool_name unless the discovered tool has no arguments. Authorization is handled by the runtime when required.",
3811
3709
  inputSchema: Type8.Object(
3812
3710
  {
3813
3711
  tool_name: Type8.String({
@@ -3824,7 +3722,11 @@ function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
3824
3722
  ),
3825
3723
  execute: async (input) => {
3826
3724
  const { tool_name } = input;
3827
- const mcpTool = mcpToolManager.getResolvedActiveTools(getActiveSkills()).find((candidate) => candidate.name === tool_name);
3725
+ const provider = parseMcpProviderFromToolName(tool_name);
3726
+ if (provider) {
3727
+ await mcpToolManager.activateProvider(provider);
3728
+ }
3729
+ const mcpTool = mcpToolManager.getResolvedActiveTools().find((candidate) => candidate.name === tool_name);
3828
3730
  if (!mcpTool) {
3829
3731
  throw new Error(`MCP tool is not active for this turn: ${tool_name}`);
3830
3732
  }
@@ -4073,6 +3975,33 @@ function scoreTool(toolDef, query) {
4073
3975
  }
4074
3976
  return score;
4075
3977
  }
3978
+ function scoreProvider(provider, query) {
3979
+ const normalizedQuery = normalize(query);
3980
+ if (!normalizedQuery) {
3981
+ return 0;
3982
+ }
3983
+ const normalizedName = normalize(provider.provider);
3984
+ const text = normalize([provider.provider, provider.description].join(" "));
3985
+ let score = 0;
3986
+ if (normalizedName === normalizedQuery) {
3987
+ score += 100;
3988
+ }
3989
+ if (normalizedName.includes(normalizedQuery)) {
3990
+ score += 50;
3991
+ }
3992
+ if (text.includes(normalizedQuery)) {
3993
+ score += 25;
3994
+ }
3995
+ for (const term of normalizedQuery.split(/\s+/).filter(Boolean)) {
3996
+ if (normalizedName.includes(term)) {
3997
+ score += 12;
3998
+ }
3999
+ if (text.includes(term)) {
4000
+ score += 4;
4001
+ }
4002
+ }
4003
+ return score;
4004
+ }
4076
4005
  function searchMcpCatalog(tools, query) {
4077
4006
  if (!normalize(query)) {
4078
4007
  return [...tools].sort(
@@ -4091,9 +4020,28 @@ function searchMcpCatalog(tools, query) {
4091
4020
  return left.tool.name.localeCompare(right.tool.name);
4092
4021
  }).map((ranked) => ranked.tool);
4093
4022
  }
4094
- function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
4023
+ function searchProviderCatalog(providers, query) {
4024
+ const sorted = [...providers].sort(
4025
+ (left, right) => left.provider.localeCompare(right.provider)
4026
+ );
4027
+ if (!normalize(query)) {
4028
+ return sorted;
4029
+ }
4030
+ return sorted.map(
4031
+ (provider) => ({
4032
+ provider,
4033
+ score: scoreProvider(provider, query)
4034
+ })
4035
+ ).filter((ranked) => ranked.score > 0).sort((left, right) => {
4036
+ if (right.score !== left.score) {
4037
+ return right.score - left.score;
4038
+ }
4039
+ return left.provider.provider.localeCompare(right.provider.provider);
4040
+ }).map((ranked) => ranked.provider);
4041
+ }
4042
+ function createSearchMcpToolsTool(mcpToolManager) {
4095
4043
  return tool({
4096
- description: "List or search active MCP tools and return full descriptors, including input/output schemas and annotations. Use after loadSkill when choosing a provider tool or when callMcpTool arguments are unclear.",
4044
+ description: "List or search MCP providers and active MCP tools. When provider is supplied and not yet active, Junior connects to it on demand and returns tool descriptors including schemas. Without provider, returns active tools plus matching configured providers without connecting. Use when choosing a provider tool or when callMcpTool arguments are unclear.",
4097
4045
  inputSchema: Type10.Object(
4098
4046
  {
4099
4047
  query: Type10.Optional(
@@ -4105,7 +4053,7 @@ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
4105
4053
  provider: Type10.Optional(
4106
4054
  Type10.String({
4107
4055
  minLength: 1,
4108
- description: "Optional provider name to list or search within."
4056
+ description: "Optional provider name to list or search within. If configured but not yet connected, Junior activates it on demand."
4109
4057
  })
4110
4058
  ),
4111
4059
  max_results: Type10.Optional(
@@ -4119,8 +4067,10 @@ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
4119
4067
  { additionalProperties: false }
4120
4068
  ),
4121
4069
  execute: async ({ query, provider, max_results }) => {
4070
+ if (provider) {
4071
+ await mcpToolManager.activateProvider(provider);
4072
+ }
4122
4073
  const catalog = mcpToolManager.getActiveToolCatalog(
4123
- getActiveSkills(),
4124
4074
  provider ? { provider } : {}
4125
4075
  );
4126
4076
  const maxResults = max_results ?? DEFAULT_MAX_RESULTS;
@@ -4128,11 +4078,16 @@ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
4128
4078
  0,
4129
4079
  maxResults
4130
4080
  );
4081
+ const providers = provider ? [] : searchProviderCatalog(
4082
+ mcpToolManager.getAvailableProviderCatalog(),
4083
+ query ?? ""
4084
+ ).slice(0, maxResults);
4131
4085
  return {
4132
4086
  query: query ?? null,
4133
4087
  provider: provider ?? null,
4134
4088
  total_active_tools: catalog.length,
4135
4089
  returned_tools: matches.length,
4090
+ available_providers: providers,
4136
4091
  tools: matches.map(toExposedToolSummary)
4137
4092
  };
4138
4093
  }
@@ -4167,11 +4122,11 @@ function sliceFileContent(params) {
4167
4122
  } : {}
4168
4123
  };
4169
4124
  }
4170
- function missingFileResult(path11) {
4125
+ function missingFileResult(path10) {
4171
4126
  return {
4172
4127
  content: "",
4173
4128
  error: "not_found",
4174
- path: path11,
4129
+ path: path10,
4175
4130
  success: false
4176
4131
  };
4177
4132
  }
@@ -6745,6 +6700,18 @@ function refreshRuntimeTurnContext(messages, turnContextPrompt) {
6745
6700
  nextMessages[index] = updated;
6746
6701
  return nextMessages;
6747
6702
  }
6703
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
6704
+ const content = getUserMessageContent(messages[index]);
6705
+ if (!content) {
6706
+ continue;
6707
+ }
6708
+ const nextMessages = [...messages];
6709
+ nextMessages[index] = {
6710
+ ...messages[index],
6711
+ content: [{ type: "text", text: turnContextPrompt }, ...content]
6712
+ };
6713
+ return nextMessages;
6714
+ }
6748
6715
  return [
6749
6716
  ...messages,
6750
6717
  {
@@ -6754,6 +6721,13 @@ function refreshRuntimeTurnContext(messages, turnContextPrompt) {
6754
6721
  }
6755
6722
  ];
6756
6723
  }
6724
+ function hasRuntimeTurnContext(messages) {
6725
+ return messages.some(
6726
+ (message) => getUserMessageContent(message)?.some(
6727
+ (part) => isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
6728
+ )
6729
+ );
6730
+ }
6757
6731
  function stripRuntimeTurnContext(messages) {
6758
6732
  return messages.flatMap((message) => {
6759
6733
  const content = getUserMessageContent(message);
@@ -6808,9 +6782,11 @@ function trimTrailingAssistantMessages(messages) {
6808
6782
  return end === messages.length ? [...messages] : messages.slice(0, end);
6809
6783
  }
6810
6784
 
6785
+ // src/chat/state/ttl.ts
6786
+ var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6787
+
6811
6788
  // src/chat/tools/advisor/session-store.ts
6812
- import { THREAD_STATE_TTL_MS } from "chat";
6813
- var ADVISOR_SESSION_TTL_MS = THREAD_STATE_TTL_MS;
6789
+ var ADVISOR_SESSION_TTL_MS = JUNIOR_THREAD_STATE_TTL_MS;
6814
6790
  function cloneMessages(messages) {
6815
6791
  return structuredClone(messages);
6816
6792
  }
@@ -6926,20 +6902,33 @@ function createAdvisorTool(context) {
6926
6902
  );
6927
6903
  }
6928
6904
  const conversationId = context.conversationId;
6905
+ const conversationPrivacy = context.conversationPrivacy ?? "private";
6906
+ const requestText = [
6907
+ "<advisor-task>",
6908
+ escapeXml(advisorQuestion),
6909
+ "</advisor-task>",
6910
+ "",
6911
+ "<executor-context>",
6912
+ escapeXml(advisorContext),
6913
+ "</executor-context>"
6914
+ ].join("\n");
6915
+ const advisorInputMessage = {
6916
+ role: "user",
6917
+ content: [
6918
+ {
6919
+ type: "text",
6920
+ text: requestText
6921
+ }
6922
+ ]
6923
+ };
6924
+ const advisorInputMessagesAttribute = serializeGenAiAttribute(
6925
+ conversationPrivacy !== "public" ? [toGenAiMessageMetadata(advisorInputMessage)] : [advisorInputMessage]
6926
+ );
6929
6927
  return await withSpan(
6930
- "ai.invoke_advisor",
6928
+ `invoke_agent ${context.config.modelId}`,
6931
6929
  "gen_ai.invoke_agent",
6932
6930
  spanContext,
6933
6931
  async () => {
6934
- const requestText = [
6935
- "<advisor-task>",
6936
- escapeXml(advisorQuestion),
6937
- "</advisor-task>",
6938
- "",
6939
- "<executor-context>",
6940
- escapeXml(advisorContext),
6941
- "</executor-context>"
6942
- ].join("\n");
6943
6932
  const requestMessage = {
6944
6933
  role: "user",
6945
6934
  content: [{ type: "text", text: requestText }],
@@ -6979,7 +6968,15 @@ function createAdvisorTool(context) {
6979
6968
  }
6980
6969
  const assistant = lastAssistantMessage(advisorAgent.state.messages);
6981
6970
  const newAdvisorMessages = advisorAgent.state.messages.slice(beforeMessageCount);
6982
- setSpanAttributes(extractGenAiUsageAttributes(...newAdvisorMessages));
6971
+ const outputMessages = newAdvisorMessages.filter(isAssistantMessage);
6972
+ const outputMessagesAttribute = serializeGenAiAttribute(
6973
+ conversationPrivacy !== "public" ? outputMessages.map(toGenAiMessageMetadata) : outputMessages
6974
+ );
6975
+ setSpanAttributes({
6976
+ ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
6977
+ ...toGenAiMessagesTraceAttributes("app.ai.output", outputMessages),
6978
+ ...extractGenAiUsageAttributes(...newAdvisorMessages)
6979
+ });
6983
6980
  if (!assistant || assistant.stopReason === "error" || assistant.stopReason === "aborted") {
6984
6981
  setSpanStatus("error");
6985
6982
  return failure(
@@ -7001,9 +6998,17 @@ function createAdvisorTool(context) {
7001
6998
  return success(memo);
7002
6999
  },
7003
7000
  {
7004
- "gen_ai.provider.name": "vercel-ai-gateway",
7001
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
7005
7002
  "gen_ai.operation.name": "invoke_agent",
7006
- "gen_ai.request.model": context.config.modelId
7003
+ "gen_ai.request.model": context.config.modelId,
7004
+ "gen_ai.output.type": "text",
7005
+ "server.address": GEN_AI_SERVER_ADDRESS,
7006
+ "server.port": GEN_AI_SERVER_PORT,
7007
+ "app.conversation.privacy": conversationPrivacy,
7008
+ ...toGenAiMessagesTraceAttributes("app.ai.input", [
7009
+ advisorInputMessage
7010
+ ]),
7011
+ ...advisorInputMessagesAttribute ? { "gen_ai.input.messages": advisorInputMessagesAttribute } : {}
7007
7012
  }
7008
7013
  );
7009
7014
  }
@@ -7743,15 +7748,9 @@ function createTools(availableSkills, hooks = {}, context) {
7743
7748
  if (context.advisor) {
7744
7749
  tools.advisor = createAdvisorTool(context.advisor);
7745
7750
  }
7746
- if (context.mcpToolManager && context.getActiveSkills) {
7747
- tools.searchMcpTools = createSearchMcpToolsTool(
7748
- context.mcpToolManager,
7749
- context.getActiveSkills
7750
- );
7751
- tools.callMcpTool = createCallMcpToolTool(
7752
- context.mcpToolManager,
7753
- context.getActiveSkills
7754
- );
7751
+ if (context.mcpToolManager) {
7752
+ tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
7753
+ tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
7755
7754
  }
7756
7755
  const { channelCapabilities } = context;
7757
7756
  if (channelCapabilities.canCreateCanvas) {
@@ -7796,49 +7795,73 @@ function resolveChannelCapabilities(channelId) {
7796
7795
  import {
7797
7796
  streamSimple
7798
7797
  } from "@earendil-works/pi-ai";
7799
- function buildChatStartAttributes(model, context) {
7798
+ function attributeModeForPrivacy(conversationPrivacy) {
7799
+ return conversationPrivacy === "public" ? "content" : "metadata";
7800
+ }
7801
+ function buildChatStartAttributes(model, context, mode, conversationPrivacy) {
7800
7802
  const attributes = {
7801
7803
  "gen_ai.operation.name": "chat",
7802
7804
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
7803
- "gen_ai.request.model": model.id
7805
+ "gen_ai.request.model": model.id,
7806
+ "gen_ai.request.stream": true,
7807
+ "gen_ai.output.type": "text",
7808
+ "server.address": GEN_AI_SERVER_ADDRESS,
7809
+ "server.port": GEN_AI_SERVER_PORT,
7810
+ ...conversationPrivacy ? { "app.conversation.privacy": conversationPrivacy } : {},
7811
+ ...toGenAiMessagesTraceAttributes("app.ai.input", context.messages)
7804
7812
  };
7805
- const inputMessages = serializeGenAiAttribute(context.messages);
7813
+ const inputMessages = serializeGenAiAttribute(
7814
+ mode === "metadata" ? context.messages.map(toGenAiMessageMetadata) : context.messages
7815
+ );
7806
7816
  if (inputMessages) {
7807
7817
  attributes["gen_ai.input.messages"] = inputMessages;
7808
7818
  }
7809
7819
  if (context.systemPrompt) {
7810
7820
  const systemInstructions = serializeGenAiAttribute([
7811
- { type: "text", content: context.systemPrompt }
7821
+ mode === "metadata" ? toGenAiTextMetadata(context.systemPrompt) : { type: "text", content: context.systemPrompt }
7812
7822
  ]);
7813
7823
  if (systemInstructions) {
7814
7824
  attributes["gen_ai.system_instructions"] = systemInstructions;
7815
7825
  }
7826
+ attributes["app.ai.system_instructions.content_chars"] = context.systemPrompt.length;
7816
7827
  }
7817
7828
  return attributes;
7818
7829
  }
7819
- function buildChatEndAttributes(message) {
7820
- const attributes = {};
7821
- const outputMessages = serializeGenAiAttribute([message]);
7830
+ function buildChatEndAttributes(message, mode) {
7831
+ const attributes = {
7832
+ ...toGenAiMessagesTraceAttributes("app.ai.output", [message])
7833
+ };
7834
+ const outputMessages = serializeGenAiAttribute(
7835
+ mode === "metadata" ? [toGenAiMessageMetadata(message)] : [message]
7836
+ );
7822
7837
  if (outputMessages) {
7823
7838
  attributes["gen_ai.output.messages"] = outputMessages;
7824
7839
  }
7825
7840
  Object.assign(attributes, extractGenAiUsageAttributes(message));
7826
7841
  if (message.stopReason) {
7827
- attributes["gen_ai.response.finish_reasons"] = [message.stopReason];
7842
+ attributes["gen_ai.response.finish_reasons"] = [
7843
+ normalizeGenAiFinishReason(message.stopReason)
7844
+ ];
7828
7845
  }
7829
7846
  if (message.model) {
7830
7847
  attributes["gen_ai.response.model"] = message.model;
7831
7848
  }
7832
7849
  return attributes;
7833
7850
  }
7834
- function createTracedStreamFn(base = streamSimple) {
7851
+ function createTracedStreamFn(baseOrOptions = streamSimple) {
7852
+ const base = typeof baseOrOptions === "function" ? baseOrOptions : baseOrOptions.base ?? streamSimple;
7853
+ const mode = attributeModeForPrivacy(
7854
+ typeof baseOrOptions === "function" ? void 0 : baseOrOptions.conversationPrivacy
7855
+ );
7856
+ const conversationPrivacy = typeof baseOrOptions === "function" ? void 0 : baseOrOptions.conversationPrivacy;
7857
+ const effectivePrivacy = conversationPrivacy ?? "private";
7835
7858
  return async (model, context, options) => {
7836
7859
  const span = sentry_exports.startInactiveSpan({
7837
7860
  name: `chat ${model.id}`,
7838
7861
  op: "gen_ai.chat",
7839
7862
  attributes: {
7840
7863
  ...getLogContextAttributes(),
7841
- ...buildChatStartAttributes(model, context)
7864
+ ...buildChatStartAttributes(model, context, mode, effectivePrivacy)
7842
7865
  }
7843
7866
  });
7844
7867
  try {
@@ -7850,7 +7873,7 @@ function createTracedStreamFn(base = streamSimple) {
7850
7873
  (finalMessage) => {
7851
7874
  try {
7852
7875
  for (const [key, value] of Object.entries(
7853
- buildChatEndAttributes(finalMessage)
7876
+ buildChatEndAttributes(finalMessage, mode)
7854
7877
  )) {
7855
7878
  span.setAttribute(key, value);
7856
7879
  }
@@ -8198,8 +8221,8 @@ function sandboxProxyUrl(requesterToken) {
8198
8221
  "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
8199
8222
  );
8200
8223
  }
8201
- const path11 = requesterToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${requesterToken}` : SANDBOX_EGRESS_PROXY_PATH;
8202
- return new URL(path11, baseUrl).toString();
8224
+ const path10 = requesterToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${requesterToken}` : SANDBOX_EGRESS_PROXY_PATH;
8225
+ return new URL(path10, baseUrl).toString();
8203
8226
  }
8204
8227
  function buildSandboxEgressNetworkPolicy(input) {
8205
8228
  const allow = {
@@ -8447,9 +8470,9 @@ import { createBashTool as createBashTool2 } from "bash-tool";
8447
8470
 
8448
8471
  // src/chat/sandbox/skill-sync.ts
8449
8472
  import fs3 from "fs/promises";
8450
- import path9 from "path";
8473
+ import path8 from "path";
8451
8474
  function toPosixRelative(base, absolute) {
8452
- return path9.relative(base, absolute).split(path9.sep).join("/");
8475
+ return path8.relative(base, absolute).split(path8.sep).join("/");
8453
8476
  }
8454
8477
  async function listFilesRecursive(root) {
8455
8478
  const queue = [root];
@@ -8459,7 +8482,7 @@ async function listFilesRecursive(root) {
8459
8482
  const entries = await fs3.readdir(dir, { withFileTypes: true });
8460
8483
  entries.sort((a, b) => a.name.localeCompare(b.name));
8461
8484
  for (const entry of entries) {
8462
- const absolute = path9.join(dir, entry.name);
8485
+ const absolute = path8.join(dir, entry.name);
8463
8486
  if (entry.isDirectory()) {
8464
8487
  queue.push(absolute);
8465
8488
  } else if (entry.isFile()) {
@@ -8498,7 +8521,7 @@ async function buildSkillSyncFiles(availableSkills, referenceFiles) {
8498
8521
  });
8499
8522
  if (referenceFiles && referenceFiles.length > 0) {
8500
8523
  for (const absoluteFile of referenceFiles) {
8501
- const fileName = path9.basename(absoluteFile);
8524
+ const fileName = path8.basename(absoluteFile);
8502
8525
  filesToWrite.push({
8503
8526
  path: `${SANDBOX_DATA_ROOT}/${fileName}`,
8504
8527
  content: await fs3.readFile(absoluteFile)
@@ -8510,7 +8533,7 @@ async function buildSkillSyncFiles(availableSkills, referenceFiles) {
8510
8533
  function collectDirectories(filesToWrite, workspaceRoot) {
8511
8534
  const directoriesToEnsure = /* @__PURE__ */ new Set();
8512
8535
  for (const file of filesToWrite) {
8513
- const normalizedPath = path9.posix.normalize(file.path);
8536
+ const normalizedPath = path8.posix.normalize(file.path);
8514
8537
  const parts = normalizedPath.split("/").filter(Boolean);
8515
8538
  let current = "";
8516
8539
  for (let index = 0; index < parts.length - 1; index += 1) {
@@ -8523,19 +8546,19 @@ function collectDirectories(filesToWrite, workspaceRoot) {
8523
8546
  ).sort((a, b) => a.length - b.length);
8524
8547
  }
8525
8548
  function resolveHostSkillPath(availableSkills, sandboxPath) {
8526
- const normalizedPath = path9.posix.normalize(sandboxPath.trim());
8549
+ const normalizedPath = path8.posix.normalize(sandboxPath.trim());
8527
8550
  for (const skill of availableSkills) {
8528
8551
  const virtualRoot = sandboxSkillDir(skill.name);
8529
8552
  if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
8530
8553
  continue;
8531
8554
  }
8532
- const relativePath = path9.posix.relative(virtualRoot, normalizedPath);
8555
+ const relativePath = path8.posix.relative(virtualRoot, normalizedPath);
8533
8556
  if (!relativePath || relativePath.startsWith("../")) {
8534
8557
  return null;
8535
8558
  }
8536
- const hostRoot = path9.resolve(skill.skillPath);
8537
- const hostPath = path9.resolve(hostRoot, ...relativePath.split("/"));
8538
- if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path9.sep}`)) {
8559
+ const hostRoot = path8.resolve(skill.skillPath);
8560
+ const hostPath = path8.resolve(hostRoot, ...relativePath.split("/"));
8561
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path8.sep}`)) {
8539
8562
  return null;
8540
8563
  }
8541
8564
  return hostPath;
@@ -8543,16 +8566,16 @@ function resolveHostSkillPath(availableSkills, sandboxPath) {
8543
8566
  return null;
8544
8567
  }
8545
8568
  function resolveHostDataPath(referenceFiles, sandboxPath) {
8546
- const normalizedPath = path9.posix.normalize(sandboxPath.trim());
8569
+ const normalizedPath = path8.posix.normalize(sandboxPath.trim());
8547
8570
  if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
8548
8571
  return null;
8549
8572
  }
8550
- const relativePath = path9.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
8573
+ const relativePath = path8.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
8551
8574
  if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
8552
8575
  return null;
8553
8576
  }
8554
8577
  for (const hostFile of referenceFiles) {
8555
- if (path9.basename(hostFile) === relativePath) {
8578
+ if (path8.basename(hostFile) === relativePath) {
8556
8579
  return hostFile;
8557
8580
  }
8558
8581
  }
@@ -8790,6 +8813,12 @@ function createSandboxSessionManager(options) {
8790
8813
  "app.sandbox.recovery.attempted": true,
8791
8814
  "app.sandbox.recovery.source": source
8792
8815
  });
8816
+ logWarn(
8817
+ "sandbox_unavailable_recreating",
8818
+ traceContext,
8819
+ { "app.sandbox.recovery.source": source },
8820
+ "Sandbox unavailable; recreating"
8821
+ );
8793
8822
  clearSession();
8794
8823
  const replacement = await createFreshSandbox();
8795
8824
  setSpanAttributes({
@@ -8929,6 +8958,15 @@ function createSandboxSessionManager(options) {
8929
8958
  } : {},
8930
8959
  ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8931
8960
  });
8961
+ logInfo(
8962
+ "sandbox_hint_discarded_profile_mismatch",
8963
+ traceContext,
8964
+ {
8965
+ ...options?.sandboxDependencyProfileHash ? { "app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash } : {},
8966
+ ...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
8967
+ },
8968
+ "Dependency profile changed; discarding sandbox hint and creating fresh session"
8969
+ );
8932
8970
  sandboxIdHint = void 0;
8933
8971
  };
8934
8972
  const tryReuseCachedSandbox = async () => {
@@ -8970,7 +9008,16 @@ function createSandboxSessionManager(options) {
8970
9008
  })
8971
9009
  )
8972
9010
  );
8973
- } catch {
9011
+ } catch (error) {
9012
+ logWarn(
9013
+ "sandbox_restore_hint_failed",
9014
+ traceContext,
9015
+ {
9016
+ "app.sandbox.hint_id": sandboxIdHint,
9017
+ "app.sandbox.error": error instanceof Error ? error.message : String(error)
9018
+ },
9019
+ "Failed to restore sandbox from hint; will create fresh session"
9020
+ );
8974
9021
  return null;
8975
9022
  }
8976
9023
  try {
@@ -9786,6 +9833,9 @@ function normalizeToolResult(result, isSandboxResult) {
9786
9833
  // src/chat/tools/execution/tool-error-handler.ts
9787
9834
  import { AgentPluginToolInputError } from "@sentry/junior-plugin-api";
9788
9835
 
9836
+ // src/chat/services/plugin-auth-orchestration.ts
9837
+ import { THREAD_STATE_TTL_MS } from "chat";
9838
+
9789
9839
  // src/chat/mcp/auth-store.ts
9790
9840
  var MCP_AUTH_SESSION_PREFIX = "junior:mcp_auth_session";
9791
9841
  var MCP_AUTH_CREDENTIALS_PREFIX = "junior:mcp_auth_credentials";
@@ -10048,317 +10098,6 @@ function buildDeterministicTurnId(messageId) {
10048
10098
  return `turn_${sanitized}`;
10049
10099
  }
10050
10100
 
10051
- // src/chat/state/turn-session-store.ts
10052
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
10053
-
10054
- // src/chat/state/pi-session-message-store.ts
10055
- import { isDeepStrictEqual } from "util";
10056
- var PI_SESSION_MESSAGE_PREFIX = "junior:pi_session_message";
10057
- function piSessionMessageKey(scope, index) {
10058
- return `${PI_SESSION_MESSAGE_PREFIX}:${scope.conversationId}:${scope.sessionId}:${index}`;
10059
- }
10060
- function parsePiMessage(value) {
10061
- return isRecord(value) ? value : void 0;
10062
- }
10063
- function normalizeMessageCount(value) {
10064
- return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
10065
- }
10066
- function countMatchingPrefix(left, right) {
10067
- const limit = Math.min(left.length, right.length);
10068
- for (let index = 0; index < limit; index += 1) {
10069
- if (!isDeepStrictEqual(left[index], right[index])) {
10070
- return index;
10071
- }
10072
- }
10073
- return limit;
10074
- }
10075
- async function loadPiSessionMessages(args) {
10076
- const stateAdapter = getStateAdapter();
10077
- await stateAdapter.connect();
10078
- const messageCount = normalizeMessageCount(args.messageCount);
10079
- if (messageCount === 0) {
10080
- return [];
10081
- }
10082
- const values = await Promise.all(
10083
- Array.from(
10084
- { length: messageCount },
10085
- (_, index) => stateAdapter.get(piSessionMessageKey(args, index))
10086
- )
10087
- );
10088
- const messages = [];
10089
- for (const value of values) {
10090
- const message = parsePiMessage(value);
10091
- if (!message) {
10092
- break;
10093
- }
10094
- messages.push(message);
10095
- }
10096
- return messages.length === messageCount ? messages : void 0;
10097
- }
10098
- async function loadExistingPiSessionMessages(scope, maxCount) {
10099
- const count = normalizeMessageCount(maxCount);
10100
- if (count === 0) {
10101
- return [];
10102
- }
10103
- const stateAdapter = getStateAdapter();
10104
- await stateAdapter.connect();
10105
- const values = await Promise.all(
10106
- Array.from(
10107
- { length: count },
10108
- (_, index) => stateAdapter.get(piSessionMessageKey(scope, index))
10109
- )
10110
- );
10111
- const messages = [];
10112
- for (const value of values) {
10113
- const message = parsePiMessage(value);
10114
- if (!message) {
10115
- break;
10116
- }
10117
- messages.push(message);
10118
- }
10119
- return messages;
10120
- }
10121
- async function commitPiSessionMessages(args) {
10122
- const stateAdapter = getStateAdapter();
10123
- await stateAdapter.connect();
10124
- const existingMessages = await loadExistingPiSessionMessages(
10125
- { conversationId: args.conversationId, sessionId: args.sessionId },
10126
- args.messages.length
10127
- );
10128
- const writeFromIndex = countMatchingPrefix(existingMessages, args.messages);
10129
- await Promise.all(
10130
- args.messages.slice(writeFromIndex).map(
10131
- (message, offset) => stateAdapter.set(
10132
- piSessionMessageKey(args, writeFromIndex + offset),
10133
- message,
10134
- args.ttlMs
10135
- )
10136
- )
10137
- );
10138
- }
10139
-
10140
- // src/chat/state/turn-session-store.ts
10141
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
10142
- var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS2;
10143
- function agentTurnSessionKey(conversationId, sessionId) {
10144
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
10145
- }
10146
- function toFiniteNonNegativeNumber(value) {
10147
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
10148
- }
10149
- function parseAgentTurnUsage(value) {
10150
- if (!isRecord(value)) {
10151
- return void 0;
10152
- }
10153
- const usage = {};
10154
- for (const field of [
10155
- "inputTokens",
10156
- "outputTokens",
10157
- "cachedInputTokens",
10158
- "cacheCreationTokens",
10159
- "totalTokens"
10160
- ]) {
10161
- const count = toFiniteNonNegativeNumber(value[field]);
10162
- if (count !== void 0) {
10163
- usage[field] = count;
10164
- }
10165
- }
10166
- return Object.keys(usage).length > 0 ? usage : void 0;
10167
- }
10168
- function parseStoredRecord(value) {
10169
- if (isRecord(value)) {
10170
- return value;
10171
- }
10172
- if (typeof value !== "string") {
10173
- return void 0;
10174
- }
10175
- try {
10176
- const parsed = JSON.parse(value);
10177
- return isRecord(parsed) ? parsed : void 0;
10178
- } catch {
10179
- return void 0;
10180
- }
10181
- }
10182
- function parseAgentTurnSessionRecord(value) {
10183
- const parsed = parseStoredRecord(value);
10184
- if (!parsed) {
10185
- return void 0;
10186
- }
10187
- const status = parsed.state;
10188
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
10189
- return void 0;
10190
- }
10191
- const conversationId = parsed.conversationId;
10192
- const sessionId = parsed.sessionId;
10193
- const sliceId = parsed.sliceId;
10194
- const checkpointVersion = parsed.checkpointVersion;
10195
- const updatedAtMs = parsed.updatedAtMs;
10196
- const cumulativeDurationMs = toFiniteNonNegativeNumber(
10197
- parsed.cumulativeDurationMs
10198
- );
10199
- const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
10200
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
10201
- return void 0;
10202
- }
10203
- const legacyPiMessages = Array.isArray(parsed.piMessages) ? parsed.piMessages : [];
10204
- const messageCount = toFiniteNonNegativeNumber(parsed.messageCount) ?? legacyPiMessages.length;
10205
- return {
10206
- legacyPiMessages,
10207
- record: {
10208
- checkpointVersion,
10209
- conversationId,
10210
- sessionId,
10211
- sliceId,
10212
- state: status,
10213
- updatedAtMs,
10214
- messageCount,
10215
- ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
10216
- ...cumulativeUsage ? { cumulativeUsage } : {},
10217
- ...Array.isArray(parsed.loadedSkillNames) ? {
10218
- loadedSkillNames: parsed.loadedSkillNames.filter(
10219
- (value2) => typeof value2 === "string"
10220
- )
10221
- } : {},
10222
- ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
10223
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
10224
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
10225
- }
10226
- };
10227
- }
10228
- function materializePiMessages(legacyPiMessages, messageCount, sessionMessages) {
10229
- if (messageCount === 0) {
10230
- return [];
10231
- }
10232
- if (sessionMessages) {
10233
- return sessionMessages;
10234
- }
10235
- if (legacyPiMessages.length >= messageCount) {
10236
- return legacyPiMessages.slice(0, messageCount);
10237
- }
10238
- return void 0;
10239
- }
10240
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
10241
- const stateAdapter = getStateAdapter();
10242
- await stateAdapter.connect();
10243
- const value = await stateAdapter.get(
10244
- agentTurnSessionKey(conversationId, sessionId)
10245
- );
10246
- const parsed = parseAgentTurnSessionRecord(value);
10247
- if (!parsed) {
10248
- return void 0;
10249
- }
10250
- const sessionMessages = await loadPiSessionMessages({
10251
- conversationId,
10252
- sessionId,
10253
- messageCount: parsed.record.messageCount
10254
- });
10255
- const piMessages = materializePiMessages(
10256
- parsed.legacyPiMessages,
10257
- parsed.record.messageCount,
10258
- sessionMessages
10259
- );
10260
- if (!piMessages) {
10261
- return void 0;
10262
- }
10263
- return {
10264
- ...parsed.record,
10265
- piMessages
10266
- };
10267
- }
10268
- async function upsertAgentTurnSessionCheckpoint(args) {
10269
- const stateAdapter = getStateAdapter();
10270
- await stateAdapter.connect();
10271
- const existingValue = await stateAdapter.get(
10272
- agentTurnSessionKey(args.conversationId, args.sessionId)
10273
- );
10274
- const existingRecord = parseAgentTurnSessionRecord(existingValue);
10275
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
10276
- await commitPiSessionMessages({
10277
- conversationId: args.conversationId,
10278
- sessionId: args.sessionId,
10279
- messages: args.piMessages,
10280
- ttlMs
10281
- });
10282
- const storedMessageCount = args.piMessages.length;
10283
- const checkpoint = {
10284
- checkpointVersion: (existingRecord?.record.checkpointVersion ?? 0) + 1,
10285
- conversationId: args.conversationId,
10286
- sessionId: args.sessionId,
10287
- sliceId: args.sliceId,
10288
- state: args.state,
10289
- updatedAtMs: Date.now(),
10290
- messageCount: storedMessageCount,
10291
- ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
10292
- cumulativeDurationMs: Math.max(
10293
- 0,
10294
- Math.floor(args.cumulativeDurationMs)
10295
- )
10296
- } : {},
10297
- ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
10298
- ...Array.isArray(args.loadedSkillNames) ? {
10299
- loadedSkillNames: args.loadedSkillNames.filter(
10300
- (value) => typeof value === "string"
10301
- )
10302
- } : {},
10303
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
10304
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
10305
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
10306
- };
10307
- await stateAdapter.set(
10308
- agentTurnSessionKey(args.conversationId, args.sessionId),
10309
- checkpoint,
10310
- ttlMs
10311
- );
10312
- return {
10313
- ...checkpoint,
10314
- piMessages: [...args.piMessages]
10315
- };
10316
- }
10317
- async function supersedeAgentTurnSessionCheckpoint(args) {
10318
- const existing = await getAgentTurnSessionCheckpoint(
10319
- args.conversationId,
10320
- args.sessionId
10321
- );
10322
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
10323
- return void 0;
10324
- }
10325
- return await upsertAgentTurnSessionCheckpoint({
10326
- conversationId: existing.conversationId,
10327
- sessionId: existing.sessionId,
10328
- sliceId: existing.sliceId,
10329
- state: "superseded",
10330
- piMessages: existing.piMessages,
10331
- cumulativeDurationMs: existing.cumulativeDurationMs,
10332
- cumulativeUsage: existing.cumulativeUsage,
10333
- loadedSkillNames: existing.loadedSkillNames,
10334
- resumeReason: existing.resumeReason,
10335
- resumedFromSliceId: existing.resumedFromSliceId,
10336
- errorMessage: args.errorMessage ?? existing.errorMessage
10337
- });
10338
- }
10339
- async function failAgentTurnSessionCheckpoint(args) {
10340
- const existing = await getAgentTurnSessionCheckpoint(
10341
- args.conversationId,
10342
- args.sessionId
10343
- );
10344
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded" || typeof args.expectedCheckpointVersion === "number" && existing.checkpointVersion !== args.expectedCheckpointVersion) {
10345
- return void 0;
10346
- }
10347
- return await upsertAgentTurnSessionCheckpoint({
10348
- conversationId: existing.conversationId,
10349
- sessionId: existing.sessionId,
10350
- sliceId: existing.sliceId,
10351
- state: "failed",
10352
- piMessages: existing.piMessages,
10353
- cumulativeDurationMs: existing.cumulativeDurationMs,
10354
- cumulativeUsage: existing.cumulativeUsage,
10355
- loadedSkillNames: existing.loadedSkillNames,
10356
- resumeReason: existing.resumeReason,
10357
- resumedFromSliceId: existing.resumedFromSliceId,
10358
- errorMessage: args.errorMessage ?? existing.errorMessage
10359
- });
10360
- }
10361
-
10362
10101
  // src/chat/services/pending-auth.ts
10363
10102
  var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
10364
10103
  function canReusePendingAuthLink(args) {
@@ -10391,10 +10130,10 @@ async function applyPendingAuthUpdate(args) {
10391
10130
  const previousPendingAuth = args.conversation.processing.pendingAuth;
10392
10131
  args.conversation.processing.pendingAuth = args.nextPendingAuth;
10393
10132
  if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
10394
- await supersedeAgentTurnSessionCheckpoint({
10133
+ await abandonAgentTurnSessionRecord({
10395
10134
  conversationId: args.conversationId,
10396
10135
  sessionId: previousPendingAuth.sessionId,
10397
- errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
10136
+ errorMessage: "Abandoned by a newer auth-blocked request in the same conversation."
10398
10137
  });
10399
10138
  }
10400
10139
  }
@@ -10514,6 +10253,9 @@ function formatCommand(command) {
10514
10253
  const collapsed = command.replace(/\s+/g, " ").trim();
10515
10254
  return collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;
10516
10255
  }
10256
+ function authorizationId(args) {
10257
+ return `${args.sessionId}:${args.kind}:${args.provider}`;
10258
+ }
10517
10259
  function buildCredentialFailureError(provider, command) {
10518
10260
  const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
10519
10261
  const plugin = getPluginDefinition(provider);
@@ -10576,6 +10318,21 @@ function createPluginAuthOrchestration(deps, abortAgent) {
10576
10318
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
10577
10319
  });
10578
10320
  }
10321
+ if (deps.conversationId && deps.sessionId) {
10322
+ await recordAuthorizationRequested({
10323
+ conversationId: deps.conversationId,
10324
+ kind: "plugin",
10325
+ provider,
10326
+ requesterId: deps.requesterId,
10327
+ authorizationId: authorizationId({
10328
+ kind: "plugin",
10329
+ provider,
10330
+ sessionId: deps.sessionId
10331
+ }),
10332
+ delivery: reusingPendingLink ? "private_link_reused" : "private_link_sent",
10333
+ ttlMs: THREAD_STATE_TTL_MS
10334
+ });
10335
+ }
10579
10336
  pendingPause = new PluginAuthorizationPauseError(
10580
10337
  provider,
10581
10338
  reusingPendingLink ? "link_already_sent" : "link_sent"
@@ -10701,8 +10458,12 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
10701
10458
  }
10702
10459
 
10703
10460
  // src/chat/tools/agent-tools.ts
10704
- function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall, agentHooks) {
10461
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall, agentHooks, conversationPrivacy) {
10705
10462
  const shouldTrace = shouldEmitDevAgentTrace();
10463
+ const effectiveConversationPrivacy = conversationPrivacy ?? "private";
10464
+ const serializeToolPayload = (payload) => serializeGenAiAttribute(
10465
+ effectiveConversationPrivacy === "private" ? toGenAiPayloadMetadata(payload) : payload
10466
+ );
10706
10467
  return Object.entries(tools).map(([toolName, toolDef]) => ({
10707
10468
  name: toolName,
10708
10469
  label: toolName,
@@ -10712,7 +10473,11 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10712
10473
  executionMode: toolDef.executionMode,
10713
10474
  execute: async (toolCallId, params) => {
10714
10475
  const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
10715
- const toolArgumentsAttribute = serializeGenAiAttribute(params);
10476
+ const toolArgumentsAttribute = serializeToolPayload(params);
10477
+ const toolArgumentsMetadata = toGenAiPayloadTraceAttributes(
10478
+ "app.ai.tool.call.arguments",
10479
+ params
10480
+ );
10716
10481
  if (toolName === "reportProgress") {
10717
10482
  const status = buildReportedProgressStatus(params);
10718
10483
  if (status) {
@@ -10728,10 +10493,14 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10728
10493
  try {
10729
10494
  if (typeof toolDef.execute !== "function") {
10730
10495
  const resultDetails = { ok: true };
10731
- const toolResultAttribute2 = serializeGenAiAttribute(resultDetails);
10496
+ const toolResultAttribute2 = serializeToolPayload(resultDetails);
10732
10497
  if (toolResultAttribute2) {
10733
10498
  setSpanAttributes({
10734
- "gen_ai.tool.call.result": toolResultAttribute2
10499
+ "gen_ai.tool.call.result": toolResultAttribute2,
10500
+ ...toGenAiPayloadTraceAttributes(
10501
+ "app.ai.tool.call.result",
10502
+ resultDetails
10503
+ )
10735
10504
  });
10736
10505
  }
10737
10506
  return {
@@ -10763,10 +10532,14 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10763
10532
  });
10764
10533
  }
10765
10534
  const resultAttributeValue = normalized.details && typeof normalized.details === "object" && "rawResult" in normalized.details && normalized.details.rawResult !== void 0 ? normalized.details.rawResult : normalized.details;
10766
- const toolResultAttribute = serializeGenAiAttribute(resultAttributeValue);
10535
+ const toolResultAttribute = serializeToolPayload(resultAttributeValue);
10767
10536
  if (toolResultAttribute) {
10768
10537
  setSpanAttributes({
10769
- "gen_ai.tool.call.result": toolResultAttribute
10538
+ "gen_ai.tool.call.result": toolResultAttribute,
10539
+ ...toGenAiPayloadTraceAttributes(
10540
+ "app.ai.tool.call.result",
10541
+ resultAttributeValue
10542
+ )
10770
10543
  });
10771
10544
  }
10772
10545
  return normalized;
@@ -10786,8 +10559,10 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10786
10559
  {
10787
10560
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
10788
10561
  "gen_ai.operation.name": "execute_tool",
10562
+ "app.conversation.privacy": effectiveConversationPrivacy,
10789
10563
  "gen_ai.tool.name": toolName,
10790
10564
  "gen_ai.tool.description": toolDef.description,
10565
+ ...toolArgumentsMetadata,
10791
10566
  ...normalizedToolCallId ? { "gen_ai.tool.call.id": normalizedToolCallId } : {},
10792
10567
  ...toolArgumentsAttribute ? { "gen_ai.tool.call.arguments": toolArgumentsAttribute } : {}
10793
10568
  }
@@ -10796,9 +10571,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10796
10571
  }));
10797
10572
  }
10798
10573
 
10799
- // src/chat/runtime/thread-state.ts
10800
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
10801
-
10802
10574
  // src/chat/configuration/validation.ts
10803
10575
  var CONFIG_KEY_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
10804
10576
  var SECRET_KEY_RE = /(?:^|[_.-])(token|secret|password|passphrase|api[-_]?key|private[-_]?key|credential|auth)(?:$|[_.-])/i;
@@ -11144,7 +10916,6 @@ function coerceThreadConversationState(value) {
11144
10916
  const processing = {
11145
10917
  activeTurnId: toOptionalString(rawProcessing.activeTurnId),
11146
10918
  lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
11147
- lastSessionId: toOptionalString(rawProcessing.lastSessionId),
11148
10919
  pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
11149
10920
  };
11150
10921
  const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
@@ -11224,6 +10995,7 @@ function coerceThreadArtifactsState(value) {
11224
10995
  }
11225
10996
  return {
11226
10997
  assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
10998
+ assistantTitle: typeof artifacts.assistantTitle === "string" ? artifacts.assistantTitle : void 0,
11227
10999
  assistantTitleSourceMessageId: typeof artifacts.assistantTitleSourceMessageId === "string" ? artifacts.assistantTitleSourceMessageId : void 0,
11228
11000
  lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
11229
11001
  lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
@@ -11322,7 +11094,11 @@ async function persistThreadStateById(threadId, patch) {
11322
11094
  await stateAdapter.connect();
11323
11095
  const key = threadStateKey(threadId);
11324
11096
  const existing = await stateAdapter.get(key) ?? {};
11325
- await stateAdapter.set(key, { ...existing, ...payload }, THREAD_STATE_TTL_MS3);
11097
+ await stateAdapter.set(
11098
+ key,
11099
+ { ...existing, ...payload },
11100
+ JUNIOR_THREAD_STATE_TTL_MS
11101
+ );
11326
11102
  }
11327
11103
  function getChannelConfigurationService(thread) {
11328
11104
  const channel = thread.channel;
@@ -11346,7 +11122,7 @@ function getChannelConfigurationServiceById(channelId) {
11346
11122
  await stateAdapter.set(
11347
11123
  key,
11348
11124
  { ...existing, configuration: state },
11349
- THREAD_STATE_TTL_MS3
11125
+ JUNIOR_THREAD_STATE_TTL_MS
11350
11126
  );
11351
11127
  }
11352
11128
  });
@@ -11389,7 +11165,6 @@ function markTurnClosed(args) {
11389
11165
  }
11390
11166
  function markTurnCompleted(args) {
11391
11167
  clearActiveTurn(args.conversation, args.sessionId);
11392
- args.conversation.processing.lastSessionId = args.sessionId;
11393
11168
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
11394
11169
  args.updateConversationStats(args.conversation);
11395
11170
  }
@@ -11931,8 +11706,8 @@ function addAgentTurnUsage(...usages) {
11931
11706
  return hasAgentTurnUsage(components) ? components : void 0;
11932
11707
  }
11933
11708
 
11934
- // src/chat/services/turn-checkpoint.ts
11935
- function logCheckpointError(error, eventName, args, attributes, message) {
11709
+ // src/chat/services/turn-session-record.ts
11710
+ function logSessionRecordError(error, eventName, args, attributes, message) {
11936
11711
  logException(
11937
11712
  error,
11938
11713
  eventName,
@@ -11965,171 +11740,201 @@ function isContinuableBoundary(messages) {
11965
11740
  const lastRole = getPiMessageRole(messages.at(-1));
11966
11741
  return lastRole === "user" || lastRole === "toolResult";
11967
11742
  }
11968
- async function loadTurnCheckpoint(ctx) {
11743
+ function resumableBoundary(messages, fallbackMessages) {
11744
+ const current = trimTrailingAssistantMessages(messages);
11745
+ if (current.length > 0 && isContinuableBoundary(current)) {
11746
+ return current;
11747
+ }
11748
+ return trimTrailingAssistantMessages(fallbackMessages ?? []);
11749
+ }
11750
+ async function loadTurnSessionRecord(ctx) {
11969
11751
  const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
11970
- const existingCheckpoint = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionCheckpoint(ctx.conversationId, ctx.sessionId) : void 0;
11971
- const hasAwaitingResumeCheckpoint = Boolean(
11972
- existingCheckpoint && existingCheckpoint.state === "awaiting_resume" && existingCheckpoint.piMessages.length > 0
11752
+ const existingSessionRecord = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionRecord(ctx.conversationId, ctx.sessionId) : void 0;
11753
+ const hasAwaitingResumeRecord = Boolean(
11754
+ existingSessionRecord && existingSessionRecord.state === "awaiting_resume" && existingSessionRecord.piMessages.length > 0
11973
11755
  );
11974
11756
  return {
11975
11757
  canUseTurnSession,
11976
- resumedFromCheckpoint: hasAwaitingResumeCheckpoint,
11977
- currentSliceId: hasAwaitingResumeCheckpoint ? existingCheckpoint.sliceId : 1,
11978
- existingCheckpoint
11758
+ resumedFromSessionRecord: hasAwaitingResumeRecord,
11759
+ currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
11760
+ existingSessionRecord
11979
11761
  };
11980
11762
  }
11981
- async function persistRunningCheckpoint(args) {
11763
+ async function persistRunningSessionRecord(args) {
11982
11764
  if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
11983
11765
  return;
11984
11766
  }
11985
11767
  try {
11986
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
11768
+ const latestSessionRecord = await getAgentTurnSessionRecord(
11987
11769
  args.conversationId,
11988
11770
  args.sessionId
11989
11771
  );
11990
- await upsertAgentTurnSessionCheckpoint({
11772
+ await upsertAgentTurnSessionRecord({
11773
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11991
11774
  conversationId: args.conversationId,
11992
- cumulativeDurationMs: latestCheckpoint?.cumulativeDurationMs,
11993
- cumulativeUsage: latestCheckpoint?.cumulativeUsage,
11775
+ cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
11776
+ cumulativeUsage: latestSessionRecord?.cumulativeUsage,
11994
11777
  sessionId: args.sessionId,
11995
11778
  sliceId: args.sliceId,
11996
11779
  state: "running",
11997
11780
  piMessages: args.messages,
11998
- loadedSkillNames: args.loadedSkillNames
11781
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11782
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11783
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11999
11784
  });
12000
- } catch (checkpointError) {
12001
- logCheckpointError(
12002
- checkpointError,
12003
- "agent_turn_running_checkpoint_failed",
11785
+ } catch (recordError) {
11786
+ logSessionRecordError(
11787
+ recordError,
11788
+ "agent_turn_running_session_record_failed",
12004
11789
  args,
12005
11790
  {
12006
11791
  "app.ai.resume_slice_id": args.sliceId
12007
11792
  },
12008
- "Failed to persist running turn checkpoint"
11793
+ "Failed to persist running turn session record"
12009
11794
  );
12010
11795
  }
12011
11796
  }
12012
- async function persistCompletedCheckpoint(args) {
11797
+ async function persistCompletedSessionRecord(args) {
12013
11798
  try {
12014
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
11799
+ const latestSessionRecord = await getAgentTurnSessionRecord(
12015
11800
  args.conversationId,
12016
11801
  args.sessionId
12017
11802
  );
12018
- await upsertAgentTurnSessionCheckpoint({
11803
+ await upsertAgentTurnSessionRecord({
11804
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
12019
11805
  conversationId: args.conversationId,
12020
11806
  cumulativeDurationMs: addDurationMs(
12021
- latestCheckpoint?.cumulativeDurationMs,
11807
+ latestSessionRecord?.cumulativeDurationMs,
12022
11808
  args.currentDurationMs
12023
11809
  ),
12024
11810
  cumulativeUsage: addAgentTurnUsage(
12025
- latestCheckpoint?.cumulativeUsage,
11811
+ latestSessionRecord?.cumulativeUsage,
12026
11812
  args.currentUsage
12027
11813
  ),
12028
11814
  sessionId: args.sessionId,
12029
11815
  sliceId: args.sliceId,
12030
11816
  state: "completed",
12031
11817
  piMessages: args.allMessages,
12032
- loadedSkillNames: args.loadedSkillNames
11818
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11819
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11820
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
12033
11821
  });
12034
- } catch (checkpointError) {
12035
- logCheckpointError(
12036
- checkpointError,
12037
- "agent_turn_completed_checkpoint_failed",
11822
+ } catch (recordError) {
11823
+ logSessionRecordError(
11824
+ recordError,
11825
+ "agent_turn_completed_session_record_failed",
12038
11826
  args,
12039
11827
  {
12040
11828
  "app.ai.resume_slice_id": args.sliceId
12041
11829
  },
12042
- "Failed to persist completed turn checkpoint"
11830
+ "Failed to persist completed turn session record"
12043
11831
  );
12044
11832
  }
12045
11833
  }
12046
- async function persistAuthPauseCheckpoint(args) {
11834
+ async function persistAuthPauseSessionRecord(args) {
12047
11835
  const nextSliceId = args.currentSliceId + 1;
12048
11836
  try {
12049
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
11837
+ const latestSessionRecord = await getAgentTurnSessionRecord(
12050
11838
  args.conversationId,
12051
11839
  args.sessionId
12052
11840
  );
12053
- const piMessages = trimTrailingAssistantMessages(
12054
- args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
11841
+ const piMessages = resumableBoundary(
11842
+ args.messages,
11843
+ latestSessionRecord?.piMessages
12055
11844
  );
12056
- return await upsertAgentTurnSessionCheckpoint({
11845
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11846
+ return void 0;
11847
+ }
11848
+ return await upsertAgentTurnSessionRecord({
11849
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
12057
11850
  conversationId: args.conversationId,
12058
11851
  cumulativeDurationMs: addDurationMs(
12059
- latestCheckpoint?.cumulativeDurationMs,
11852
+ latestSessionRecord?.cumulativeDurationMs,
12060
11853
  args.currentDurationMs
12061
11854
  ),
12062
11855
  cumulativeUsage: addAgentTurnUsage(
12063
- latestCheckpoint?.cumulativeUsage,
11856
+ latestSessionRecord?.cumulativeUsage,
12064
11857
  args.currentUsage
12065
11858
  ),
12066
11859
  sessionId: args.sessionId,
12067
11860
  sliceId: nextSliceId,
12068
11861
  state: "awaiting_resume",
12069
11862
  piMessages,
12070
- loadedSkillNames: args.loadedSkillNames,
11863
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
12071
11864
  resumeReason: "auth",
12072
11865
  resumedFromSliceId: args.currentSliceId,
12073
- errorMessage: args.errorMessage
11866
+ errorMessage: args.errorMessage,
11867
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11868
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
12074
11869
  });
12075
- } catch (checkpointError) {
12076
- logCheckpointError(
12077
- checkpointError,
12078
- "agent_turn_auth_resume_checkpoint_failed",
11870
+ } catch (recordError) {
11871
+ logSessionRecordError(
11872
+ recordError,
11873
+ "agent_turn_auth_resume_session_record_failed",
12079
11874
  args,
12080
11875
  {
12081
11876
  "app.ai.resume_from_slice_id": args.currentSliceId,
12082
11877
  "app.ai.resume_next_slice_id": nextSliceId
12083
11878
  },
12084
- "Failed to persist auth checkpoint before retry"
11879
+ "Failed to persist auth session record before retry"
12085
11880
  );
12086
11881
  }
12087
11882
  return void 0;
12088
11883
  }
12089
- async function persistTimeoutCheckpoint(args) {
11884
+ async function persistTimeoutSessionRecord(args) {
12090
11885
  const nextSliceId = args.currentSliceId + 1;
12091
11886
  try {
12092
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
11887
+ const latestSessionRecord = await getAgentTurnSessionRecord(
12093
11888
  args.conversationId,
12094
11889
  args.sessionId
12095
11890
  );
12096
- const piMessages = trimTrailingAssistantMessages(
12097
- args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
11891
+ const piMessages = resumableBoundary(
11892
+ args.messages,
11893
+ latestSessionRecord?.piMessages
12098
11894
  );
12099
- return await upsertAgentTurnSessionCheckpoint({
11895
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11896
+ return void 0;
11897
+ }
11898
+ return await upsertAgentTurnSessionRecord({
11899
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
12100
11900
  conversationId: args.conversationId,
12101
11901
  cumulativeDurationMs: addDurationMs(
12102
- latestCheckpoint?.cumulativeDurationMs,
11902
+ latestSessionRecord?.cumulativeDurationMs,
12103
11903
  args.currentDurationMs
12104
11904
  ),
12105
11905
  cumulativeUsage: addAgentTurnUsage(
12106
- latestCheckpoint?.cumulativeUsage,
11906
+ latestSessionRecord?.cumulativeUsage,
12107
11907
  args.currentUsage
12108
11908
  ),
12109
11909
  sessionId: args.sessionId,
12110
11910
  sliceId: nextSliceId,
12111
11911
  state: "awaiting_resume",
12112
11912
  piMessages,
12113
- loadedSkillNames: args.loadedSkillNames,
11913
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
12114
11914
  resumeReason: "timeout",
12115
11915
  resumedFromSliceId: args.currentSliceId,
12116
- errorMessage: args.errorMessage
11916
+ errorMessage: args.errorMessage,
11917
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11918
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
12117
11919
  });
12118
- } catch (checkpointError) {
12119
- logCheckpointError(
12120
- checkpointError,
12121
- "agent_turn_timeout_resume_checkpoint_failed",
11920
+ } catch (recordError) {
11921
+ logSessionRecordError(
11922
+ recordError,
11923
+ "agent_turn_timeout_resume_session_record_failed",
12122
11924
  args,
12123
11925
  {
12124
11926
  "app.ai.resume_from_slice_id": args.currentSliceId,
12125
11927
  "app.ai.resume_next_slice_id": nextSliceId
12126
11928
  },
12127
- "Failed to persist timeout checkpoint before scheduling resume"
11929
+ "Failed to persist timeout session record before scheduling resume"
12128
11930
  );
12129
11931
  return void 0;
12130
11932
  }
12131
11933
  }
12132
11934
 
11935
+ // src/chat/services/mcp-auth-orchestration.ts
11936
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
11937
+
12133
11938
  // src/chat/mcp/oauth.ts
12134
11939
  import { randomUUID as randomUUID3 } from "crypto";
12135
11940
  import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -12397,6 +12202,9 @@ var McpAuthorizationPauseError = class extends AuthorizationPauseError {
12397
12202
  super("mcp", provider, disposition);
12398
12203
  }
12399
12204
  };
12205
+ function authorizationId2(args) {
12206
+ return `${args.sessionId}:${args.kind}:${args.provider}`;
12207
+ }
12400
12208
  function createMcpAuthOrchestration(deps, abortAgent) {
12401
12209
  let pendingPause;
12402
12210
  const authSessionIdsByProvider = /* @__PURE__ */ new Map();
@@ -12473,6 +12281,21 @@ function createMcpAuthOrchestration(deps, abortAgent) {
12473
12281
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
12474
12282
  });
12475
12283
  }
12284
+ if (deps.conversationId && deps.sessionId && deps.requesterId) {
12285
+ await recordAuthorizationRequested({
12286
+ conversationId: deps.conversationId,
12287
+ kind: "mcp",
12288
+ provider,
12289
+ requesterId: deps.requesterId,
12290
+ authorizationId: authorizationId2({
12291
+ kind: "mcp",
12292
+ provider,
12293
+ sessionId: deps.sessionId
12294
+ }),
12295
+ delivery: reusingPendingLink ? "private_link_reused" : "private_link_sent",
12296
+ ttlMs: THREAD_STATE_TTL_MS2
12297
+ });
12298
+ }
12476
12299
  pendingPause = new McpAuthorizationPauseError(
12477
12300
  provider,
12478
12301
  reusingPendingLink ? "link_already_sent" : "link_sent"
@@ -12517,6 +12340,15 @@ function extractSliceUsage(messages, beforeMessageCount) {
12517
12340
  );
12518
12341
  return hasAgentTurnUsage(usage) ? usage : void 0;
12519
12342
  }
12343
+ function requesterFromContext(requester, requesterId) {
12344
+ const identity = {
12345
+ ...requester?.email ? { email: requester.email } : {},
12346
+ ...requester?.fullName ? { fullName: requester.fullName } : {},
12347
+ ...requesterId ?? requester?.userId ? { slackUserId: requesterId ?? requester?.userId } : {},
12348
+ ...requester?.userName ? { slackUserName: requester.userName } : {}
12349
+ };
12350
+ return Object.keys(identity).length > 0 ? identity : void 0;
12351
+ }
12520
12352
  function supportsRouterTextPreview(mediaType) {
12521
12353
  const baseMediaType = mediaType.split(";", 1)[0]?.trim().toLowerCase();
12522
12354
  if (!baseMediaType) {
@@ -12604,11 +12436,21 @@ async function generateAssistantReply(messageText2, context = {}) {
12604
12436
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
12605
12437
  let loadedSkillNamesForResume = [];
12606
12438
  let mcpToolManager;
12439
+ let connectedMcpProviders = /* @__PURE__ */ new Set();
12440
+ let canRecordMcpProviders = false;
12607
12441
  let sandboxExecutor;
12608
12442
  let timedOut = false;
12609
12443
  let turnUsage;
12610
12444
  let thinkingSelection;
12611
- const checkpointLogContext = {
12445
+ const requester = requesterFromContext(
12446
+ context.requester,
12447
+ context.correlation?.requesterId
12448
+ );
12449
+ const conversationPrivacy = resolveConversationPrivacy({
12450
+ channelId: context.correlation?.channelId,
12451
+ conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId
12452
+ });
12453
+ const sessionRecordLogContext = {
12612
12454
  threadId: context.correlation?.threadId,
12613
12455
  requesterId: context.correlation?.requesterId,
12614
12456
  channelId: context.correlation?.channelId,
@@ -12618,6 +12460,25 @@ async function generateAssistantReply(messageText2, context = {}) {
12618
12460
  assistantUserName: botConfig.userName,
12619
12461
  modelId: botConfig.modelId
12620
12462
  };
12463
+ const recordConnectedMcpProvider = async (provider) => {
12464
+ if (!canRecordMcpProviders || !timeoutResumeConversationId || connectedMcpProviders.has(provider)) {
12465
+ return;
12466
+ }
12467
+ await recordMcpProviderConnected({
12468
+ conversationId: timeoutResumeConversationId,
12469
+ provider,
12470
+ ttlMs: THREAD_STATE_TTL_MS3
12471
+ });
12472
+ connectedMcpProviders.add(provider);
12473
+ };
12474
+ const recordActiveMcpProviders = async () => {
12475
+ if (!mcpToolManager) {
12476
+ return;
12477
+ }
12478
+ for (const provider of mcpToolManager.getActiveProviders()) {
12479
+ await recordConnectedMcpProvider(provider);
12480
+ }
12481
+ };
12621
12482
  const getSandboxMetadata = () => sandboxExecutor ? {
12622
12483
  sandboxId: sandboxExecutor.getSandboxId(),
12623
12484
  sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
@@ -12685,16 +12546,22 @@ async function generateAssistantReply(messageText2, context = {}) {
12685
12546
  const skillInvocation = parseSkillInvocation(userInput, availableSkills);
12686
12547
  const invokedSkill = skillInvocation ? findSkillByName(skillInvocation.skillName, availableSkills) : null;
12687
12548
  const activeSkills = [];
12549
+ const syncLoadedSkillNamesForResume = () => {
12550
+ loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
12551
+ };
12688
12552
  const skillSandbox = new SkillSandbox(availableSkills, activeSkills);
12689
12553
  const { conversationId: sessionConversationId, sessionId } = getSessionIdentifiers(context);
12690
- const checkpointState = await loadTurnCheckpoint({
12554
+ const turnSessionState = await loadTurnSessionRecord({
12691
12555
  conversationId: sessionConversationId,
12692
12556
  sessionId
12693
12557
  });
12694
- const { resumedFromCheckpoint, currentSliceId, existingCheckpoint } = checkpointState;
12558
+ const { resumedFromSessionRecord, currentSliceId, existingSessionRecord } = turnSessionState;
12695
12559
  timeoutResumeConversationId = sessionConversationId;
12696
12560
  timeoutResumeSessionId = sessionId;
12697
12561
  timeoutResumeSliceId = currentSliceId;
12562
+ canRecordMcpProviders = Boolean(
12563
+ turnSessionState.canUseTurnSession && sessionConversationId && sessionId
12564
+ );
12698
12565
  const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
12699
12566
  configurationValues = {
12700
12567
  ...getConfigDefaults(),
@@ -12738,6 +12605,12 @@ async function generateAssistantReply(messageText2, context = {}) {
12738
12605
  const currentSandboxExecutor = sandboxExecutor;
12739
12606
  sandboxExecutor.configureSkills(availableSkills);
12740
12607
  sandboxExecutor.configureReferenceFiles(listReferenceFiles());
12608
+ const priorPiMessages = resumedFromSessionRecord ? existingSessionRecord?.piMessages : context.piMessages;
12609
+ connectedMcpProviders = new Set(
12610
+ turnSessionState.canUseTurnSession && sessionConversationId ? await loadConnectedMcpProviders({
12611
+ conversationId: sessionConversationId
12612
+ }) : []
12613
+ );
12741
12614
  let sandboxPromise;
12742
12615
  let sandboxPromiseId;
12743
12616
  const clearSandboxPromise = () => {
@@ -12782,16 +12655,20 @@ async function generateAssistantReply(messageText2, context = {}) {
12782
12655
  cwd: input.cwd
12783
12656
  })).runCommand(input)
12784
12657
  };
12785
- for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
12786
- const preloaded = await skillSandbox.loadSkill(skillName);
12787
- if (preloaded) {
12788
- upsertActiveSkill(activeSkills, preloaded);
12658
+ for (const skillName of inferLoadedSkillNamesFromPiMessages(
12659
+ priorPiMessages
12660
+ )) {
12661
+ const restoredSkill = await skillSandbox.loadSkill(skillName);
12662
+ if (restoredSkill) {
12663
+ upsertActiveSkill(activeSkills, restoredSkill);
12664
+ syncLoadedSkillNamesForResume();
12789
12665
  }
12790
12666
  }
12791
12667
  if (invokedSkill) {
12792
- const preloaded = await skillSandbox.loadSkill(invokedSkill.name);
12793
- if (preloaded) {
12794
- upsertActiveSkill(activeSkills, preloaded);
12668
+ const restoredSkill = await skillSandbox.loadSkill(invokedSkill.name);
12669
+ if (restoredSkill) {
12670
+ upsertActiveSkill(activeSkills, restoredSkill);
12671
+ syncLoadedSkillNamesForResume();
12795
12672
  }
12796
12673
  }
12797
12674
  const promptConversationContext = context.piMessages && context.piMessages.length > 0 ? void 0 : context.conversationContext;
@@ -12804,6 +12681,14 @@ async function generateAssistantReply(messageText2, context = {}) {
12804
12681
  userAttachments: context.userAttachments,
12805
12682
  userTurnText
12806
12683
  });
12684
+ const preAgentPromptMessages = () => existingSessionRecord?.piMessages ?? [
12685
+ ...context.piMessages ?? [],
12686
+ {
12687
+ role: "user",
12688
+ content: userContentParts,
12689
+ timestamp: Date.now()
12690
+ }
12691
+ ];
12807
12692
  thinkingSelection = await selectTurnThinkingLevel({
12808
12693
  completeObject,
12809
12694
  conversationContext: context.conversationContext,
@@ -12872,9 +12757,6 @@ async function generateAssistantReply(messageText2, context = {}) {
12872
12757
  });
12873
12758
  const turnMcpToolManager = mcpToolManager;
12874
12759
  const getPendingAuthPause = () => pluginAuth.getPendingPause() ?? mcpAuth.getPendingPause();
12875
- const syncResumeState = () => {
12876
- loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
12877
- };
12878
12760
  setTags({
12879
12761
  conversationId: spanContext.conversationId,
12880
12762
  slackThreadId: context.correlation?.threadId,
@@ -12915,9 +12797,10 @@ async function generateAssistantReply(messageText2, context = {}) {
12915
12797
  const resolvedSkill = await skillSandbox.loadSkill(loadedSkill.name);
12916
12798
  const effective = resolvedSkill ?? loadedSkill;
12917
12799
  upsertActiveSkill(activeSkills, effective);
12918
- syncResumeState();
12919
- await turnMcpToolManager.activateForSkill(effective);
12920
- syncResumeState();
12800
+ syncLoadedSkillNamesForResume();
12801
+ if (await turnMcpToolManager.activateForSkill(effective)) {
12802
+ await recordConnectedMcpProvider(effective.pluginProvider);
12803
+ }
12921
12804
  if (mcpAuth.getPendingPause()) {
12922
12805
  return void 0;
12923
12806
  }
@@ -12927,12 +12810,9 @@ async function generateAssistantReply(messageText2, context = {}) {
12927
12810
  if (!turnMcpToolManager.getActiveProviders().includes(effective.pluginProvider)) {
12928
12811
  return void 0;
12929
12812
  }
12930
- const availableToolCount = turnMcpToolManager.getActiveToolCatalog(
12931
- activeSkills,
12932
- {
12933
- provider: effective.pluginProvider
12934
- }
12935
- ).length;
12813
+ const availableToolCount = turnMcpToolManager.getActiveToolCatalog({
12814
+ provider: effective.pluginProvider
12815
+ }).length;
12936
12816
  return {
12937
12817
  mcp_provider: effective.pluginProvider,
12938
12818
  available_tool_count: availableToolCount
@@ -12949,15 +12829,15 @@ async function generateAssistantReply(messageText2, context = {}) {
12949
12829
  userText: userInput,
12950
12830
  artifactState: context.artifactState,
12951
12831
  configuration: configurationValues,
12952
- getActiveSkills: () => activeSkills,
12953
12832
  mcpToolManager: turnMcpToolManager,
12954
12833
  sandbox,
12955
12834
  advisor: {
12956
12835
  config: botConfig.advisor,
12957
12836
  conversationId: sessionConversationId,
12837
+ conversationPrivacy,
12958
12838
  logContext: spanContext,
12959
12839
  getTools: () => advisorTools,
12960
- streamFn: createTracedStreamFn()
12840
+ streamFn: createTracedStreamFn({ conversationPrivacy })
12961
12841
  }
12962
12842
  }
12963
12843
  );
@@ -12968,24 +12848,37 @@ async function generateAssistantReply(messageText2, context = {}) {
12968
12848
  promptGuidelines: definition.promptGuidelines,
12969
12849
  promptSnippet: definition.promptSnippet
12970
12850
  }));
12971
- syncResumeState();
12851
+ const providersToRestore = /* @__PURE__ */ new Set([
12852
+ ...connectedMcpProviders,
12853
+ ...inferActiveMcpProvidersFromPiMessages(priorPiMessages)
12854
+ ]);
12855
+ for (const provider of providersToRestore) {
12856
+ if (await turnMcpToolManager.activateProvider(provider)) {
12857
+ await recordConnectedMcpProvider(provider);
12858
+ }
12859
+ if (mcpAuth.getPendingPause()) {
12860
+ timeoutResumeMessages = preAgentPromptMessages();
12861
+ throw mcpAuth.getPendingPause();
12862
+ }
12863
+ }
12972
12864
  for (const skill of activeSkills) {
12973
- await turnMcpToolManager.activateForSkill(skill);
12974
- syncResumeState();
12865
+ if (await turnMcpToolManager.activateForSkill(skill)) {
12866
+ await recordConnectedMcpProvider(skill.pluginProvider);
12867
+ }
12975
12868
  if (mcpAuth.getPendingPause()) {
12976
- timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
12869
+ timeoutResumeMessages = preAgentPromptMessages();
12977
12870
  throw mcpAuth.getPendingPause();
12978
12871
  }
12979
12872
  }
12980
- syncResumeState();
12981
12873
  const activeMcpCatalogs = toActiveMcpCatalogSummaries(
12982
- turnMcpToolManager.getActiveToolCatalog(activeSkills)
12874
+ turnMcpToolManager.getActiveToolCatalog()
12983
12875
  );
12984
12876
  baseInstructions = buildSystemPrompt();
12877
+ const includeSessionContext = resumedFromSessionRecord || !hasRuntimeTurnContext(priorPiMessages ?? []);
12985
12878
  const turnContextPrompt = buildTurnContextPrompt({
12986
12879
  availableSkills,
12987
- activeSkills,
12988
12880
  activeMcpCatalogs,
12881
+ includeSessionContext,
12989
12882
  toolGuidance,
12990
12883
  runtime: {
12991
12884
  conversationId: spanContext.conversationId,
@@ -12994,14 +12887,14 @@ async function generateAssistantReply(messageText2, context = {}) {
12994
12887
  invocation: skillInvocation,
12995
12888
  requester: context.requester,
12996
12889
  artifactState: context.artifactState,
12997
- configuration: configurationValues,
12998
- turnState: resumedFromCheckpoint ? "resumed" : "fresh"
12890
+ configuration: configurationValues
12999
12891
  });
12892
+ const turnContextParts = turnContextPrompt ? [{ type: "text", text: turnContextPrompt }] : [];
13000
12893
  const promptContentParts = [
13001
- { type: "text", text: turnContextPrompt },
12894
+ ...turnContextParts,
13002
12895
  ...userContentParts
13003
12896
  ];
13004
- const inputMessagesAttribute = serializeGenAiAttribute([
12897
+ const inputMessages = [
13005
12898
  {
13006
12899
  role: "system",
13007
12900
  content: [{ type: "text", text: baseInstructions }]
@@ -13010,7 +12903,10 @@ async function generateAssistantReply(messageText2, context = {}) {
13010
12903
  role: "user",
13011
12904
  content: promptContentParts.map((part) => toObservablePromptPart(part))
13012
12905
  }
13013
- ]);
12906
+ ];
12907
+ const inputMessagesAttribute = serializeGenAiAttribute(
12908
+ conversationPrivacy !== "public" ? inputMessages.map(toGenAiMessageMetadata) : inputMessages
12909
+ );
13014
12910
  const onToolCall = (toolName, params) => {
13015
12911
  toolCalls.push(toolName);
13016
12912
  try {
@@ -13035,7 +12931,8 @@ async function generateAssistantReply(messageText2, context = {}) {
13035
12931
  sandboxExecutor,
13036
12932
  pluginAuth,
13037
12933
  onToolCall,
13038
- agentPluginHooks
12934
+ agentPluginHooks,
12935
+ conversationPrivacy
13039
12936
  );
13040
12937
  advisorTools = createAgentTools(
13041
12938
  createAdvisorToolDefinitions(tools),
@@ -13045,11 +12942,12 @@ async function generateAssistantReply(messageText2, context = {}) {
13045
12942
  sandboxExecutor,
13046
12943
  pluginAuth,
13047
12944
  onToolCall,
13048
- agentPluginHooks
12945
+ agentPluginHooks,
12946
+ conversationPrivacy
13049
12947
  );
13050
12948
  agent = new Agent2({
13051
12949
  getApiKey: () => getPiGatewayApiKeyOverride(),
13052
- streamFn: createTracedStreamFn(),
12950
+ streamFn: createTracedStreamFn({ conversationPrivacy }),
13053
12951
  initialState: {
13054
12952
  systemPrompt: baseInstructions,
13055
12953
  model: resolveGatewayModel(botConfig.modelId),
@@ -13060,16 +12958,18 @@ async function generateAssistantReply(messageText2, context = {}) {
13060
12958
  let hasEmittedText = false;
13061
12959
  let needsSeparator = false;
13062
12960
  const persistSafeBoundary = async (messages) => {
13063
- if (!checkpointState.canUseTurnSession || !sessionConversationId || !sessionId) {
12961
+ if (!turnSessionState.canUseTurnSession || !sessionConversationId || !sessionId) {
13064
12962
  return;
13065
12963
  }
13066
- await persistRunningCheckpoint({
12964
+ await persistRunningSessionRecord({
12965
+ channelName: context.correlation?.channelName,
13067
12966
  conversationId: sessionConversationId,
13068
12967
  sessionId,
13069
12968
  sliceId: currentSliceId,
13070
12969
  messages,
13071
12970
  loadedSkillNames: loadedSkillNamesForResume,
13072
- logContext: checkpointLogContext
12971
+ logContext: sessionRecordLogContext,
12972
+ requester
13073
12973
  });
13074
12974
  };
13075
12975
  const unsubscribe = agent.subscribe((event) => {
@@ -13113,17 +13013,17 @@ async function generateAssistantReply(messageText2, context = {}) {
13113
13013
  let newMessages = [];
13114
13014
  beforeMessageCount = agent.state.messages.length;
13115
13015
  try {
13116
- if (resumedFromCheckpoint) {
13117
- agent.state.messages = refreshRuntimeTurnContext(
13118
- existingCheckpoint.piMessages,
13016
+ if (resumedFromSessionRecord) {
13017
+ agent.state.messages = turnContextPrompt ? refreshRuntimeTurnContext(
13018
+ existingSessionRecord.piMessages,
13119
13019
  turnContextPrompt
13120
- );
13020
+ ) : existingSessionRecord.piMessages;
13121
13021
  } else if (context.piMessages && context.piMessages.length > 0) {
13122
13022
  agent.state.messages = [...context.piMessages];
13123
13023
  }
13124
13024
  beforeMessageCount = agent.state.messages.length;
13125
13025
  await withSpan(
13126
- "ai.generate_assistant_reply",
13026
+ `invoke_agent ${botConfig.modelId}`,
13127
13027
  "gen_ai.invoke_agent",
13128
13028
  spanContext,
13129
13029
  async () => {
@@ -13133,7 +13033,7 @@ async function generateAssistantReply(messageText2, context = {}) {
13133
13033
  content: promptContentParts,
13134
13034
  timestamp: Date.now()
13135
13035
  };
13136
- if (!resumedFromCheckpoint) {
13036
+ if (!resumedFromSessionRecord) {
13137
13037
  await persistSafeBoundary([
13138
13038
  ...agent.state.messages,
13139
13039
  freshPromptMessage
@@ -13185,13 +13085,15 @@ async function generateAssistantReply(messageText2, context = {}) {
13185
13085
  }
13186
13086
  }
13187
13087
  };
13188
- let run = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
13088
+ let run = resumedFromSessionRecord ? agent.continue() : agent.prompt(freshPromptMessage);
13189
13089
  let retryUsage;
13190
13090
  for (let attempt = 0; ; attempt += 1) {
13191
13091
  promptResult = await runAgentStep(run);
13192
13092
  newMessages = agent.state.messages.slice(beforeMessageCount);
13193
13093
  const outputMessages = newMessages.filter(isAssistantMessage);
13194
- const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
13094
+ const outputMessagesAttribute = serializeGenAiAttribute(
13095
+ conversationPrivacy !== "public" ? outputMessages.map(toGenAiMessageMetadata) : outputMessages
13096
+ );
13195
13097
  const usageSummary = extractGenAiUsageSummary(
13196
13098
  promptResult,
13197
13099
  agent.state,
@@ -13201,6 +13103,10 @@ async function generateAssistantReply(messageText2, context = {}) {
13201
13103
  turnUsage = addAgentTurnUsage(retryUsage, currentUsage);
13202
13104
  setSpanAttributes({
13203
13105
  ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
13106
+ ...toGenAiMessagesTraceAttributes(
13107
+ "app.ai.output",
13108
+ outputMessages
13109
+ ),
13204
13110
  ...extractGenAiUsageAttributes(usageSummary)
13205
13111
  });
13206
13112
  if (getPendingAuthPause()) {
@@ -13235,23 +13141,34 @@ async function generateAssistantReply(messageText2, context = {}) {
13235
13141
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
13236
13142
  "gen_ai.operation.name": "invoke_agent",
13237
13143
  "gen_ai.request.model": botConfig.modelId,
13144
+ "gen_ai.output.type": "text",
13145
+ "server.address": GEN_AI_SERVER_ADDRESS,
13146
+ "server.port": GEN_AI_SERVER_PORT,
13147
+ ...conversationPrivacy ? { "app.conversation.privacy": conversationPrivacy } : {},
13148
+ ...sessionConversationId ? { "app.ai.session.conversation_id": sessionConversationId } : {},
13149
+ ...sessionId ? { "app.ai.turn.session_id": sessionId } : {},
13150
+ ...timeoutResumeSliceId ? { "app.ai.turn.slice_id": timeoutResumeSliceId } : {},
13238
13151
  "app.ai.reasoning_effort": thinkingSelection.thinkingLevel,
13152
+ ...toGenAiMessagesTraceAttributes("app.ai.input", inputMessages),
13239
13153
  ...inputMessagesAttribute ? { "gen_ai.input.messages": inputMessagesAttribute } : {}
13240
13154
  }
13241
13155
  );
13242
13156
  } finally {
13243
13157
  unsubscribe();
13244
13158
  }
13245
- if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
13246
- await persistCompletedCheckpoint({
13159
+ if (turnSessionState.canUseTurnSession && sessionConversationId && sessionId) {
13160
+ await recordActiveMcpProviders();
13161
+ await persistCompletedSessionRecord({
13162
+ channelName: context.correlation?.channelName,
13247
13163
  conversationId: sessionConversationId,
13248
13164
  currentDurationMs: Date.now() - replyStartedAtMs,
13249
13165
  currentUsage: turnUsage,
13250
13166
  sessionId,
13251
13167
  sliceId: currentSliceId,
13252
13168
  allMessages: agent.state.messages,
13253
- loadedSkillNames: activeSkills.map((skill) => skill.name),
13254
- logContext: checkpointLogContext
13169
+ loadedSkillNames: loadedSkillNamesForResume,
13170
+ logContext: sessionRecordLogContext,
13171
+ requester
13255
13172
  });
13256
13173
  }
13257
13174
  return buildTurnResult({
@@ -13274,26 +13191,29 @@ async function generateAssistantReply(messageText2, context = {}) {
13274
13191
  } catch (error) {
13275
13192
  if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
13276
13193
  turnUsage = turnUsage ?? extractSliceUsage(timeoutResumeMessages, beforeMessageCount);
13277
- const checkpoint = await persistTimeoutCheckpoint({
13194
+ await recordActiveMcpProviders();
13195
+ const sessionRecord = await persistTimeoutSessionRecord({
13196
+ channelName: context.correlation?.channelName,
13278
13197
  conversationId: timeoutResumeConversationId,
13279
13198
  sessionId: timeoutResumeSessionId,
13280
13199
  currentSliceId: timeoutResumeSliceId,
13281
13200
  currentDurationMs: Date.now() - replyStartedAtMs,
13282
13201
  currentUsage: turnUsage,
13283
13202
  messages: timeoutResumeMessages,
13284
- loadedSkillNames: loadedSkillNamesForResume,
13285
13203
  errorMessage: error instanceof Error ? error.message : String(error),
13286
- logContext: checkpointLogContext
13204
+ loadedSkillNames: loadedSkillNamesForResume,
13205
+ logContext: sessionRecordLogContext,
13206
+ requester
13287
13207
  });
13288
- if (checkpoint) {
13208
+ if (sessionRecord) {
13289
13209
  throw new RetryableTurnError(
13290
13210
  "turn_timeout_resume",
13291
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId} version=${checkpoint.checkpointVersion}`,
13211
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${sessionRecord.sliceId} version=${sessionRecord.version}`,
13292
13212
  {
13293
13213
  conversationId: timeoutResumeConversationId,
13294
13214
  sessionId: timeoutResumeSessionId,
13295
- sliceId: checkpoint.sliceId,
13296
- checkpointVersion: checkpoint.checkpointVersion
13215
+ sliceId: sessionRecord.sliceId,
13216
+ version: sessionRecord.version
13297
13217
  }
13298
13218
  );
13299
13219
  }
@@ -13305,21 +13225,24 @@ async function generateAssistantReply(messageText2, context = {}) {
13305
13225
  beforeMessageCount
13306
13226
  );
13307
13227
  }
13308
- const checkpoint = await persistAuthPauseCheckpoint({
13228
+ await recordActiveMcpProviders();
13229
+ const sessionRecord = await persistAuthPauseSessionRecord({
13230
+ channelName: context.correlation?.channelName,
13309
13231
  conversationId: timeoutResumeConversationId,
13310
13232
  sessionId: timeoutResumeSessionId,
13311
13233
  currentSliceId: timeoutResumeSliceId,
13312
13234
  currentDurationMs: Date.now() - replyStartedAtMs,
13313
13235
  currentUsage: turnUsage,
13314
13236
  messages: timeoutResumeMessages,
13315
- loadedSkillNames: loadedSkillNamesForResume,
13316
13237
  errorMessage: error.message,
13317
- logContext: checkpointLogContext
13238
+ loadedSkillNames: loadedSkillNamesForResume,
13239
+ logContext: sessionRecordLogContext,
13240
+ requester
13318
13241
  });
13319
- if (checkpoint) {
13242
+ if (sessionRecord) {
13320
13243
  throw new RetryableTurnError(
13321
13244
  error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
13322
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId}`,
13245
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${sessionRecord.sliceId}`,
13323
13246
  {
13324
13247
  authDisposition: error.disposition,
13325
13248
  authDurationMs: Date.now() - replyStartedAtMs,
@@ -13329,7 +13252,7 @@ async function generateAssistantReply(messageText2, context = {}) {
13329
13252
  authUsage: turnUsage,
13330
13253
  conversationId: timeoutResumeConversationId,
13331
13254
  sessionId: timeoutResumeSessionId,
13332
- sliceId: checkpoint.sliceId
13255
+ sliceId: sessionRecord.sliceId
13333
13256
  }
13334
13257
  );
13335
13258
  }
@@ -13586,8 +13509,8 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
13586
13509
  "Keep the summary factual and concise. Do not invent details.",
13587
13510
  "",
13588
13511
  "Output exactly three XML sections in this order:",
13589
- "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or superseded by a later turn. Omit the section body if none. </active-asks>",
13590
- "<superseded-or-completed-asks> one bullet per ask that has been rescoped, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </superseded-or-completed-asks>",
13512
+ "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or replaced by a later turn. Omit the section body if none. </active-asks>",
13513
+ "<resolved-or-replaced-asks> one bullet per ask that has been replaced, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </resolved-or-replaced-asks>",
13591
13514
  "<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
13592
13515
  "",
13593
13516
  "Do not output any text outside the three sections.",
@@ -13740,40 +13663,6 @@ function escapeSlackMrkdwn(text) {
13740
13663
  function escapeSlackLinkUrl(url) {
13741
13664
  return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
13742
13665
  }
13743
- function getSentryOrgSlug() {
13744
- const slug = process.env.SENTRY_ORG_SLUG?.trim();
13745
- return slug || void 0;
13746
- }
13747
- function isSentrySaasDsnHost(host) {
13748
- return host === "sentry.io" || host.endsWith(".sentry.io");
13749
- }
13750
- function buildSentryWebBaseUrl(dsn) {
13751
- if (isSentrySaasDsnHost(dsn.host)) {
13752
- return "https://sentry.io";
13753
- }
13754
- const port = dsn.port ? `:${dsn.port}` : "";
13755
- const path11 = dsn.path ? `/${dsn.path}` : "";
13756
- return `${dsn.protocol}://${dsn.host}${port}${path11}`;
13757
- }
13758
- function getSentryConversationUrl(conversationId) {
13759
- const client2 = sentry_exports.getClient();
13760
- const dsn = client2?.getDsn();
13761
- if (!dsn?.host || !dsn.projectId) {
13762
- return void 0;
13763
- }
13764
- const orgSlug = getSentryOrgSlug();
13765
- if (!orgSlug) {
13766
- return void 0;
13767
- }
13768
- const encodedId = encodeURIComponent(conversationId);
13769
- const params = new URLSearchParams();
13770
- params.set("project", dsn.projectId);
13771
- const path11 = `explore/conversations/${encodedId}/?${params.toString()}`;
13772
- if (isSentrySaasDsnHost(dsn.host)) {
13773
- return `https://${orgSlug}.sentry.io/${path11}`;
13774
- }
13775
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path11}`;
13776
- }
13777
13666
  function formatSlackTokenCount(value) {
13778
13667
  if (value >= 1e6) {
13779
13668
  const millions = value / 1e6;
@@ -13827,7 +13716,7 @@ function buildSlackReplyFooter(args) {
13827
13716
  label: "ID",
13828
13717
  value: conversationId
13829
13718
  };
13830
- const conversationUrl = getSentryConversationUrl(conversationId);
13719
+ const conversationUrl = buildSentryConversationUrl(conversationId);
13831
13720
  if (conversationUrl) {
13832
13721
  idItem.url = conversationUrl;
13833
13722
  }
@@ -14130,17 +14019,17 @@ function canScheduleTurnTimeoutResume(nextSliceId) {
14130
14019
  return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
14131
14020
  }
14132
14021
  async function getAwaitingTurnContinuationRequest(args) {
14133
- const checkpoint = await getAgentTurnSessionCheckpoint(
14022
+ const sessionRecord = await getAgentTurnSessionRecord(
14134
14023
  args.conversationId,
14135
14024
  args.sessionId
14136
14025
  );
14137
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || !canScheduleTurnTimeoutResume(checkpoint.sliceId)) {
14026
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" || !canScheduleTurnTimeoutResume(sessionRecord.sliceId)) {
14138
14027
  return void 0;
14139
14028
  }
14140
14029
  return {
14141
14030
  conversationId: args.conversationId,
14142
14031
  sessionId: args.sessionId,
14143
- expectedCheckpointVersion: checkpoint.checkpointVersion
14032
+ expectedVersion: sessionRecord.version
14144
14033
  };
14145
14034
  }
14146
14035
  function getTurnTimeoutResumeSecret() {
@@ -14166,13 +14055,14 @@ function parseTurnTimeoutResumeRequest(value) {
14166
14055
  return void 0;
14167
14056
  }
14168
14057
  const record = value;
14169
- if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof record.expectedCheckpointVersion !== "number") {
14058
+ const expectedVersion = typeof record.expectedVersion === "number" ? record.expectedVersion : record.expectedCheckpointVersion;
14059
+ if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number") {
14170
14060
  return void 0;
14171
14061
  }
14172
14062
  return {
14173
14063
  conversationId: record.conversationId,
14174
14064
  sessionId: record.sessionId,
14175
- expectedCheckpointVersion: record.expectedCheckpointVersion
14065
+ expectedVersion
14176
14066
  };
14177
14067
  }
14178
14068
  async function scheduleTurnTimeoutResume(request) {
@@ -14328,7 +14218,6 @@ async function verifyDispatchCallbackRequest(request) {
14328
14218
 
14329
14219
  // src/chat/agent-dispatch/store.ts
14330
14220
  import { createHash as createHash2 } from "crypto";
14331
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
14332
14221
  var DISPATCH_PREFIX = "junior:agent_dispatch";
14333
14222
  var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
14334
14223
  var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
@@ -14419,7 +14308,7 @@ async function syncIncompleteDispatchIndex(state, record) {
14419
14308
  await state.set(
14420
14309
  incompleteDispatchIndexKey(),
14421
14310
  next.slice(-DISPATCH_INDEX_MAX_LENGTH),
14422
- THREAD_STATE_TTL_MS4
14311
+ JUNIOR_THREAD_STATE_TTL_MS
14423
14312
  );
14424
14313
  });
14425
14314
  }
@@ -14427,7 +14316,7 @@ async function putRecord(state, record) {
14427
14316
  await state.set(
14428
14317
  getDispatchStorageKey(record.id),
14429
14318
  record,
14430
- THREAD_STATE_TTL_MS4
14319
+ JUNIOR_THREAD_STATE_TTL_MS
14431
14320
  );
14432
14321
  await syncIncompleteDispatchIndex(state, record);
14433
14322
  }
@@ -14541,7 +14430,6 @@ async function markDispatch(args) {
14541
14430
  ...current,
14542
14431
  status: args.status,
14543
14432
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
14544
- ...typeof args.resumeCheckpointVersion === "number" ? { resumeCheckpointVersion: args.resumeCheckpointVersion } : {},
14545
14433
  ...args.resultMessageTs ? { resultMessageTs: args.resultMessageTs } : {}
14546
14434
  });
14547
14435
  });
@@ -14774,12 +14662,11 @@ async function runAgentDispatchSlice(callback, deps = {}) {
14774
14662
  return;
14775
14663
  }
14776
14664
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
14777
- const checkpointVersion = error.metadata?.checkpointVersion;
14665
+ const version = error.metadata?.version;
14778
14666
  const nextSliceId = error.metadata?.sliceId;
14779
- if (typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14667
+ if (typeof version === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14780
14668
  const awaiting = await markDispatch({
14781
14669
  dispatch,
14782
- resumeCheckpointVersion: checkpointVersion,
14783
14670
  status: "awaiting_resume"
14784
14671
  });
14785
14672
  await scheduleCallback({
@@ -15125,7 +15012,7 @@ function verifyHeartbeatRequest(request) {
15125
15012
  const expected = Buffer.from(secret);
15126
15013
  return actual.length === expected.length && timingSafeEqual4(actual, expected);
15127
15014
  }
15128
- async function GET4(request, waitUntil) {
15015
+ async function GET2(request, waitUntil) {
15129
15016
  if (!verifyHeartbeatRequest(request)) {
15130
15017
  return new Response("Unauthorized", { status: 401 });
15131
15018
  }
@@ -15144,6 +15031,9 @@ async function GET4(request, waitUntil) {
15144
15031
  return new Response("Accepted", { status: 202 });
15145
15032
  }
15146
15033
 
15034
+ // src/handlers/mcp-oauth-callback.ts
15035
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
15036
+
15147
15037
  // src/chat/runtime/delivered-turn-state.ts
15148
15038
  function buildDeliveredTurnStatePatch(args) {
15149
15039
  const conversation = structuredClone(args.conversation);
@@ -15490,7 +15380,7 @@ function createSlackAdapterStatusSender(args) {
15490
15380
  };
15491
15381
  }
15492
15382
  function createSlackWebApiStatusSender(args) {
15493
- const getClient3 = args.getSlackClient ?? getSlackClient;
15383
+ const getClient2 = args.getSlackClient ?? getSlackClient;
15494
15384
  return async (text, loadingMessages) => {
15495
15385
  const channelId = args.channelId;
15496
15386
  const threadTs = args.threadTs;
@@ -15503,7 +15393,7 @@ function createSlackWebApiStatusSender(args) {
15503
15393
  }
15504
15394
  const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
15505
15395
  try {
15506
- await getClient3().assistant.threads.setStatus({
15396
+ await getClient2().assistant.threads.setStatus({
15507
15397
  channel_id: normalizedChannelId,
15508
15398
  thread_ts: threadTs,
15509
15399
  status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
@@ -15649,52 +15539,6 @@ function getWorkspaceTeamId() {
15649
15539
  return workspaceTeamIdStorage.getStore();
15650
15540
  }
15651
15541
 
15652
- // src/chat/slack/context.ts
15653
- function toTrimmedSlackString(value) {
15654
- const normalized = toOptionalString(value);
15655
- return normalized?.trim() || void 0;
15656
- }
15657
- function parseSlackThreadId(threadId) {
15658
- const normalizedThreadId = toTrimmedSlackString(threadId);
15659
- if (!normalizedThreadId) {
15660
- return void 0;
15661
- }
15662
- const parts = normalizedThreadId.split(":");
15663
- if (parts.length !== 3 || parts[0] !== "slack") {
15664
- return void 0;
15665
- }
15666
- const channelId = toTrimmedSlackString(parts[1]);
15667
- const threadTs = toTrimmedSlackString(parts[2]);
15668
- if (!channelId || !threadTs) {
15669
- return void 0;
15670
- }
15671
- return { channelId, threadTs };
15672
- }
15673
- function resolveSlackChannelIdFromThreadId(threadId) {
15674
- return parseSlackThreadId(threadId)?.channelId;
15675
- }
15676
- function resolveSlackChannelIdFromMessage(message) {
15677
- const messageChannelId = toTrimmedSlackString(
15678
- message.channelId
15679
- );
15680
- if (messageChannelId) {
15681
- return messageChannelId;
15682
- }
15683
- const raw = message.raw;
15684
- if (raw && typeof raw === "object") {
15685
- const rawChannel = toTrimmedSlackString(
15686
- raw.channel
15687
- );
15688
- if (rawChannel) {
15689
- return rawChannel;
15690
- }
15691
- }
15692
- const threadId = toTrimmedSlackString(
15693
- message.threadId
15694
- );
15695
- return resolveSlackChannelIdFromThreadId(threadId);
15696
- }
15697
-
15698
15542
  // src/chat/runtime/thread-context.ts
15699
15543
  function toSlackTeamId(value) {
15700
15544
  const candidate = toOptionalString(value);
@@ -16070,7 +15914,7 @@ async function resumeSlackTurn(args) {
16070
15914
  status.start();
16071
15915
  const generateReply = runArgs.generateReply ?? generateAssistantReply;
16072
15916
  const replyContext = createResumeReplyContext(runArgs, status);
16073
- const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
15917
+ const priorSessionRecord = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionRecord(
16074
15918
  replyContext.correlation.conversationId,
16075
15919
  replyContext.correlation.turnId
16076
15920
  ) : void 0;
@@ -16097,10 +15941,10 @@ async function resumeSlackTurn(args) {
16097
15941
  await status.stop();
16098
15942
  const footer = buildSlackReplyFooter({
16099
15943
  conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey,
16100
- durationMs: typeof priorCheckpoint?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorCheckpoint?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
15944
+ durationMs: typeof priorSessionRecord?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorSessionRecord?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
16101
15945
  thinkingLevel: reply.diagnostics.thinkingLevel,
16102
15946
  usage: addAgentTurnUsage(
16103
- priorCheckpoint?.cumulativeUsage,
15947
+ priorSessionRecord?.cumulativeUsage,
16104
15948
  reply.diagnostics.usage
16105
15949
  ) ?? reply.diagnostics.usage
16106
15950
  });
@@ -16291,6 +16135,9 @@ var CALLBACK_PAGES = {
16291
16135
  status: 500
16292
16136
  }
16293
16137
  };
16138
+ function mcpAuthorizationId(args) {
16139
+ return `${args.sessionId}:mcp:${args.provider}`;
16140
+ }
16294
16141
  function htmlResponse(kind) {
16295
16142
  const page = CALLBACK_PAGES[kind];
16296
16143
  return htmlCallbackResponse(page.title, page.message, page.status);
@@ -16312,27 +16159,28 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
16312
16159
  ...statePatch
16313
16160
  });
16314
16161
  }
16315
- async function failCheckpointBestEffort(args) {
16162
+ async function failSessionRecordBestEffort(args) {
16316
16163
  try {
16317
- await failAgentTurnSessionCheckpoint({
16164
+ await failAgentTurnSessionRecord({
16318
16165
  conversationId: args.conversationId,
16319
16166
  sessionId: args.sessionId,
16320
- errorMessage: args.errorMessage
16167
+ errorMessage: args.errorMessage,
16168
+ expectedVersion: args.expectedVersion
16321
16169
  });
16322
16170
  } catch (error) {
16323
16171
  logException(
16324
16172
  error,
16325
- "mcp_oauth_callback_checkpoint_fail_persist_failed",
16173
+ "mcp_oauth_callback_session_record_fail_persist_failed",
16326
16174
  {},
16327
16175
  {
16328
16176
  "app.ai.conversation_id": args.conversationId,
16329
16177
  "app.ai.session_id": args.sessionId
16330
16178
  },
16331
- "Failed to mark MCP OAuth-resumed turn checkpoint failed"
16179
+ "Failed to mark MCP OAuth-resumed turn session record failed"
16332
16180
  );
16333
16181
  }
16334
16182
  }
16335
- async function persistFailedReplyState(channelId, threadTs, sessionId) {
16183
+ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedVersion) {
16336
16184
  const threadId = `slack:${channelId}:${threadTs}`;
16337
16185
  const currentState = await getPersistedThreadState(threadId);
16338
16186
  const conversation = coerceThreadConversationState(currentState);
@@ -16345,10 +16193,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
16345
16193
  markConversationMessage,
16346
16194
  updateConversationStats
16347
16195
  });
16348
- await failCheckpointBestEffort({
16196
+ await failSessionRecordBestEffort({
16349
16197
  conversationId: threadId,
16350
16198
  sessionId,
16351
- errorMessage: "OAuth-resumed MCP turn failed"
16199
+ errorMessage: "OAuth-resumed MCP turn failed",
16200
+ expectedVersion
16352
16201
  });
16353
16202
  await persistThreadStateById(threadId, {
16354
16203
  conversation
@@ -16374,10 +16223,10 @@ async function resumeAuthorizedMcpTurn(args) {
16374
16223
  if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
16375
16224
  clearPendingAuth(conversation, pendingAuth.sessionId);
16376
16225
  await persistThreadStateById(threadId, { conversation });
16377
- await supersedeAgentTurnSessionCheckpoint({
16226
+ await abandonAgentTurnSessionRecord({
16378
16227
  conversationId: authSession.conversationId,
16379
16228
  sessionId: pendingAuth.sessionId,
16380
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16229
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16381
16230
  });
16382
16231
  return;
16383
16232
  }
@@ -16414,10 +16263,10 @@ async function resumeAuthorizedMcpTurn(args) {
16414
16263
  await persistThreadStateById(threadId, {
16415
16264
  conversation: lockedConversation
16416
16265
  });
16417
- await supersedeAgentTurnSessionCheckpoint({
16266
+ await abandonAgentTurnSessionRecord({
16418
16267
  conversationId: authSession.conversationId,
16419
16268
  sessionId: lockedPendingAuth.sessionId,
16420
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16269
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16421
16270
  });
16422
16271
  return false;
16423
16272
  }
@@ -16431,6 +16280,13 @@ async function resumeAuthorizedMcpTurn(args) {
16431
16280
  if (!lockedUserMessage) {
16432
16281
  return false;
16433
16282
  }
16283
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
16284
+ authSession.conversationId,
16285
+ lockedSessionId
16286
+ );
16287
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16288
+ return false;
16289
+ }
16434
16290
  const lockedConversationContext = buildConversationContext(
16435
16291
  lockedConversation,
16436
16292
  {
@@ -16440,6 +16296,17 @@ async function resumeAuthorizedMcpTurn(args) {
16440
16296
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
16441
16297
  authSession.channelId
16442
16298
  );
16299
+ await recordAuthorizationCompleted({
16300
+ conversationId: authSession.conversationId,
16301
+ kind: "mcp",
16302
+ provider,
16303
+ requesterId: authSession.userId,
16304
+ authorizationId: mcpAuthorizationId({
16305
+ provider,
16306
+ sessionId: lockedSessionId
16307
+ }),
16308
+ ttlMs: THREAD_STATE_TTL_MS4
16309
+ });
16443
16310
  return {
16444
16311
  messageText: lockedUserMessage.text,
16445
16312
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
@@ -16485,8 +16352,9 @@ async function resumeAuthorizedMcpTurn(args) {
16485
16352
  );
16486
16353
  },
16487
16354
  onPostDeliveryCommitFailure: async () => {
16488
- await failAgentTurnSessionCheckpoint({
16355
+ await failAgentTurnSessionRecord({
16489
16356
  conversationId: authSession.conversationId,
16357
+ expectedVersion: lockedSessionRecord.version,
16490
16358
  sessionId: lockedSessionId,
16491
16359
  errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
16492
16360
  });
@@ -16496,7 +16364,8 @@ async function resumeAuthorizedMcpTurn(args) {
16496
16364
  await persistFailedReplyState(
16497
16365
  authSession.channelId,
16498
16366
  authSession.threadTs,
16499
- lockedSessionId
16367
+ lockedSessionId,
16368
+ lockedSessionRecord.version
16500
16369
  );
16501
16370
  } catch (persistError) {
16502
16371
  logException(
@@ -16527,11 +16396,11 @@ async function resumeAuthorizedMcpTurn(args) {
16527
16396
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16528
16397
  throw error;
16529
16398
  }
16530
- const checkpointVersion = error.metadata?.checkpointVersion;
16399
+ const version = error.metadata?.version;
16531
16400
  const nextSliceId = error.metadata?.sliceId;
16532
- if (typeof checkpointVersion !== "number") {
16401
+ if (typeof version !== "number") {
16533
16402
  throw new Error(
16534
- "Timed-out MCP resume did not include a checkpoint version"
16403
+ "Timed-out MCP resume did not include a turn-session version"
16535
16404
  );
16536
16405
  }
16537
16406
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -16551,14 +16420,14 @@ async function resumeAuthorizedMcpTurn(args) {
16551
16420
  await scheduleTurnTimeoutResume({
16552
16421
  conversationId: authSession.conversationId,
16553
16422
  sessionId: lockedSessionId,
16554
- expectedCheckpointVersion: checkpointVersion
16423
+ expectedVersion: version
16555
16424
  });
16556
16425
  }
16557
16426
  };
16558
16427
  }
16559
16428
  });
16560
16429
  }
16561
- async function GET5(request, provider, waitUntil) {
16430
+ async function GET3(request, provider, waitUntil) {
16562
16431
  const url = new URL(request.url);
16563
16432
  const state = url.searchParams.get("state")?.trim();
16564
16433
  const code = url.searchParams.get("code")?.trim();
@@ -16604,9 +16473,12 @@ async function GET5(request, provider, waitUntil) {
16604
16473
  }
16605
16474
  }
16606
16475
 
16476
+ // src/handlers/oauth-callback.ts
16477
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS5 } from "chat";
16478
+
16607
16479
  // src/chat/slack/app-home.ts
16608
16480
  import fs5 from "fs";
16609
- import path10 from "path";
16481
+ import path9 from "path";
16610
16482
  var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
16611
16483
  var MAX_HOME_SKILLS = 6;
16612
16484
  var MAX_SECTION_TEXT_CHARS = 3e3;
@@ -16618,7 +16490,7 @@ function clampSectionText(text) {
16618
16490
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
16619
16491
  }
16620
16492
  function loadDescriptionText() {
16621
- const descriptionPath = path10.join(homeDir(), "DESCRIPTION.md");
16493
+ const descriptionPath = path9.join(homeDir(), "DESCRIPTION.md");
16622
16494
  try {
16623
16495
  const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
16624
16496
  if (raw.length > 0) {
@@ -16769,23 +16641,27 @@ async function persistCompletedOAuthReplyState(args) {
16769
16641
  ...statePatch
16770
16642
  });
16771
16643
  }
16772
- async function failCheckpointBestEffort2(args) {
16644
+ function pluginAuthorizationId(args) {
16645
+ return `${args.sessionId}:plugin:${args.provider}`;
16646
+ }
16647
+ async function failSessionRecordBestEffort2(args) {
16773
16648
  try {
16774
- await failAgentTurnSessionCheckpoint({
16649
+ await failAgentTurnSessionRecord({
16775
16650
  conversationId: args.conversationId,
16651
+ expectedVersion: args.expectedVersion,
16776
16652
  sessionId: args.sessionId,
16777
16653
  errorMessage: args.errorMessage
16778
16654
  });
16779
16655
  } catch (error) {
16780
16656
  logException(
16781
16657
  error,
16782
- "oauth_callback_checkpoint_fail_persist_failed",
16658
+ "oauth_callback_session_record_fail_persist_failed",
16783
16659
  {},
16784
16660
  {
16785
16661
  "app.ai.conversation_id": args.conversationId,
16786
16662
  "app.ai.session_id": args.sessionId
16787
16663
  },
16788
- "Failed to mark OAuth-resumed turn checkpoint failed"
16664
+ "Failed to mark OAuth-resumed turn session record failed"
16789
16665
  );
16790
16666
  }
16791
16667
  }
@@ -16801,8 +16677,9 @@ async function persistFailedOAuthReplyState(args) {
16801
16677
  markConversationMessage,
16802
16678
  updateConversationStats
16803
16679
  });
16804
- await failCheckpointBestEffort2({
16680
+ await failSessionRecordBestEffort2({
16805
16681
  conversationId: args.conversationId,
16682
+ expectedVersion: args.expectedVersion,
16806
16683
  sessionId: args.sessionId,
16807
16684
  errorMessage: "OAuth-resumed turn failed"
16808
16685
  });
@@ -16810,21 +16687,21 @@ async function persistFailedOAuthReplyState(args) {
16810
16687
  conversation
16811
16688
  });
16812
16689
  }
16813
- async function resumeCheckpointedOAuthTurn(stored) {
16690
+ async function resumeOAuthSessionRecordTurn(stored) {
16814
16691
  if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
16815
16692
  return false;
16816
16693
  }
16817
- const checkpoint = await getAgentTurnSessionCheckpoint(
16694
+ const sessionRecord = await getAgentTurnSessionRecord(
16818
16695
  stored.resumeConversationId,
16819
16696
  stored.resumeSessionId
16820
16697
  );
16821
- if (!checkpoint) {
16698
+ if (!sessionRecord) {
16822
16699
  return false;
16823
16700
  }
16824
- if (checkpoint.state === "completed" || checkpoint.state === "failed" || checkpoint.state === "superseded") {
16701
+ if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16825
16702
  return true;
16826
16703
  }
16827
- if (checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
16704
+ if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16828
16705
  return true;
16829
16706
  }
16830
16707
  const currentState = await getPersistedThreadState(
@@ -16845,10 +16722,10 @@ async function resumeCheckpointedOAuthTurn(stored) {
16845
16722
  await persistThreadStateById(stored.resumeConversationId, {
16846
16723
  conversation
16847
16724
  });
16848
- await supersedeAgentTurnSessionCheckpoint({
16725
+ await abandonAgentTurnSessionRecord({
16849
16726
  conversationId: stored.resumeConversationId,
16850
16727
  sessionId: pendingAuth.sessionId,
16851
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16728
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16852
16729
  });
16853
16730
  return true;
16854
16731
  }
@@ -16871,11 +16748,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
16871
16748
  lockKey: stored.resumeConversationId,
16872
16749
  initialText: "",
16873
16750
  beforeStart: async () => {
16874
- const lockedCheckpoint = await getAgentTurnSessionCheckpoint(
16751
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
16875
16752
  stored.resumeConversationId,
16876
16753
  stored.resumeSessionId
16877
16754
  );
16878
- if (!lockedCheckpoint || lockedCheckpoint.state !== "awaiting_resume" || lockedCheckpoint.resumeReason !== "auth") {
16755
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16879
16756
  return false;
16880
16757
  }
16881
16758
  const lockedState = await getPersistedThreadState(
@@ -16899,10 +16776,10 @@ async function resumeCheckpointedOAuthTurn(stored) {
16899
16776
  await persistThreadStateById(stored.resumeConversationId, {
16900
16777
  conversation: lockedConversation
16901
16778
  });
16902
- await supersedeAgentTurnSessionCheckpoint({
16779
+ await abandonAgentTurnSessionRecord({
16903
16780
  conversationId: stored.resumeConversationId,
16904
16781
  sessionId: lockedPendingAuth.sessionId,
16905
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16782
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16906
16783
  });
16907
16784
  return false;
16908
16785
  }
@@ -16925,6 +16802,17 @@ async function resumeCheckpointedOAuthTurn(stored) {
16925
16802
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
16926
16803
  stored.channelId
16927
16804
  );
16805
+ await recordAuthorizationCompleted({
16806
+ conversationId: stored.resumeConversationId,
16807
+ kind: "plugin",
16808
+ provider: stored.provider,
16809
+ requesterId: stored.userId,
16810
+ authorizationId: pluginAuthorizationId({
16811
+ provider: stored.provider,
16812
+ sessionId: lockedSessionId
16813
+ }),
16814
+ ttlMs: THREAD_STATE_TTL_MS5
16815
+ });
16928
16816
  return {
16929
16817
  messageText: stored.pendingMessage ?? lockedUserMessage.text,
16930
16818
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
@@ -16969,7 +16857,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
16969
16857
  "app.ai.outcome": reply.diagnostics.outcome,
16970
16858
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
16971
16859
  },
16972
- "OAuth callback auto-resumed checkpoint finished replying"
16860
+ "OAuth callback auto-resumed session record finished replying"
16973
16861
  );
16974
16862
  await persistCompletedOAuthReplyState({
16975
16863
  conversationId: stored.resumeConversationId,
@@ -16978,9 +16866,9 @@ async function resumeCheckpointedOAuthTurn(stored) {
16978
16866
  });
16979
16867
  },
16980
16868
  onPostDeliveryCommitFailure: async () => {
16981
- await failAgentTurnSessionCheckpoint({
16869
+ await failAgentTurnSessionRecord({
16982
16870
  conversationId: stored.resumeConversationId,
16983
- expectedCheckpointVersion: lockedCheckpoint.checkpointVersion,
16871
+ expectedVersion: lockedSessionRecord.version,
16984
16872
  sessionId: lockedSessionId,
16985
16873
  errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
16986
16874
  });
@@ -16988,6 +16876,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
16988
16876
  onFailure: async () => {
16989
16877
  await persistFailedOAuthReplyState({
16990
16878
  conversationId: stored.resumeConversationId,
16879
+ expectedVersion: lockedSessionRecord.version,
16991
16880
  sessionId: lockedSessionId
16992
16881
  });
16993
16882
  },
@@ -17001,11 +16890,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
17001
16890
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
17002
16891
  throw error;
17003
16892
  }
17004
- const checkpointVersion = error.metadata?.checkpointVersion;
16893
+ const version = error.metadata?.version;
17005
16894
  const nextSliceId = error.metadata?.sliceId;
17006
- if (typeof checkpointVersion !== "number") {
16895
+ if (typeof version !== "number") {
17007
16896
  throw new Error(
17008
- "Timed-out OAuth resume did not include a checkpoint version"
16897
+ "Timed-out OAuth resume did not include a turn-session version"
17009
16898
  );
17010
16899
  }
17011
16900
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -17016,7 +16905,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
17016
16905
  await scheduleTurnTimeoutResume({
17017
16906
  conversationId: stored.resumeConversationId,
17018
16907
  sessionId: lockedSessionId,
17019
- expectedCheckpointVersion: checkpointVersion
16908
+ expectedVersion: version
17020
16909
  });
17021
16910
  }
17022
16911
  };
@@ -17060,7 +16949,7 @@ async function resumePendingOAuthMessage(stored) {
17060
16949
  }
17061
16950
  });
17062
16951
  }
17063
- async function GET6(request, provider, waitUntil) {
16952
+ async function GET4(request, provider, waitUntil) {
17064
16953
  const providerConfig = getPluginOAuthConfig(provider);
17065
16954
  if (!providerConfig) {
17066
16955
  return htmlErrorResponse(
@@ -17199,7 +17088,7 @@ async function GET6(request, provider, waitUntil) {
17199
17088
  if (stored.pendingMessage && stored.channelId && stored.threadTs) {
17200
17089
  waitUntil(async () => {
17201
17090
  try {
17202
- const resumed = await resumeCheckpointedOAuthTurn(stored);
17091
+ const resumed = await resumeOAuthSessionRecordTurn(stored);
17203
17092
  if (!resumed) {
17204
17093
  await resumePendingOAuthMessage(stored);
17205
17094
  }
@@ -17431,12 +17320,12 @@ function normalizePort(value) {
17431
17320
  function sandboxIdFromPayload(payload) {
17432
17321
  return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
17433
17322
  }
17434
- function normalizedForwardedPath(path11) {
17435
- if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
17323
+ function normalizedForwardedPath(path10) {
17324
+ if (!path10.startsWith("/") || path10.startsWith("//") || path10.includes("#") || /[\r\n]/.test(path10)) {
17436
17325
  return { ok: false, error: "Invalid forwarded path" };
17437
17326
  }
17438
17327
  try {
17439
- const url = new URL(path11, "https://sandbox-forwarded.local");
17328
+ const url = new URL(path10, "https://sandbox-forwarded.local");
17440
17329
  return { ok: true, path: `${url.pathname}${url.search}` };
17441
17330
  } catch {
17442
17331
  return { ok: false, error: "Invalid forwarded path" };
@@ -17471,13 +17360,13 @@ function buildUpstreamUrl(request) {
17471
17360
  if (forwardedPort && !port) {
17472
17361
  return { ok: false, error: "Invalid forwarded port" };
17473
17362
  }
17474
- const path11 = upstreamPath(request);
17475
- if (!path11.ok) {
17476
- return { ok: false, error: path11.error };
17363
+ const path10 = upstreamPath(request);
17364
+ if (!path10.ok) {
17365
+ return { ok: false, error: path10.error };
17477
17366
  }
17478
17367
  try {
17479
17368
  const url = new URL(
17480
- `${scheme}://${host}${port ? `:${port}` : ""}${path11.path}`
17369
+ `${scheme}://${host}${port ? `:${port}` : ""}${path10.path}`
17481
17370
  );
17482
17371
  return { ok: true, url };
17483
17372
  } catch {
@@ -17794,63 +17683,65 @@ function sleep4(ms) {
17794
17683
  }
17795
17684
  async function persistCompletedReplyState2(args) {
17796
17685
  const currentState = await getPersistedThreadState(
17797
- args.checkpoint.conversationId
17686
+ args.sessionRecord.conversationId
17798
17687
  );
17799
17688
  const conversation = coerceThreadConversationState(currentState);
17800
17689
  const artifacts = coerceThreadArtifactsState(currentState);
17801
17690
  const userMessage2 = getTurnUserMessage(
17802
17691
  conversation,
17803
- args.checkpoint.sessionId
17692
+ args.sessionRecord.sessionId
17804
17693
  );
17805
17694
  const statePatch = buildDeliveredTurnStatePatch({
17806
17695
  artifacts,
17807
17696
  conversation,
17808
17697
  reply: args.reply,
17809
- sessionId: args.checkpoint.sessionId,
17698
+ sessionId: args.sessionRecord.sessionId,
17810
17699
  userMessageId: userMessage2?.id
17811
17700
  });
17812
- await persistThreadStateById(args.checkpoint.conversationId, {
17701
+ await persistThreadStateById(args.sessionRecord.conversationId, {
17813
17702
  ...statePatch
17814
17703
  });
17815
17704
  }
17816
- async function failCheckpointBestEffort3(args) {
17705
+ async function failSessionRecordBestEffort3(args) {
17817
17706
  try {
17818
- await failAgentTurnSessionCheckpoint({
17819
- conversationId: args.checkpoint.conversationId,
17820
- expectedCheckpointVersion: args.checkpoint.checkpointVersion,
17821
- sessionId: args.checkpoint.sessionId,
17707
+ await failAgentTurnSessionRecord({
17708
+ conversationId: args.sessionRecord.conversationId,
17709
+ expectedVersion: args.sessionRecord.version,
17710
+ sessionId: args.sessionRecord.sessionId,
17822
17711
  errorMessage: args.errorMessage
17823
17712
  });
17824
17713
  } catch (error) {
17825
17714
  logException(
17826
17715
  error,
17827
- "timeout_resume_checkpoint_fail_persist_failed",
17716
+ "timeout_resume_session_record_fail_persist_failed",
17828
17717
  {},
17829
17718
  {
17830
- "app.ai.conversation_id": args.checkpoint.conversationId,
17831
- "app.ai.session_id": args.checkpoint.sessionId
17719
+ "app.ai.conversation_id": args.sessionRecord.conversationId,
17720
+ "app.ai.session_id": args.sessionRecord.sessionId
17832
17721
  },
17833
- "Failed to mark timed-out turn checkpoint failed"
17722
+ "Failed to mark timed-out turn session record failed"
17834
17723
  );
17835
17724
  }
17836
17725
  }
17837
- async function persistFailedReplyState2(checkpoint) {
17838
- const currentState = await getPersistedThreadState(checkpoint.conversationId);
17726
+ async function persistFailedReplyState2(sessionRecord) {
17727
+ const currentState = await getPersistedThreadState(
17728
+ sessionRecord.conversationId
17729
+ );
17839
17730
  const conversation = coerceThreadConversationState(currentState);
17840
- clearPendingAuth(conversation, checkpoint.sessionId);
17731
+ clearPendingAuth(conversation, sessionRecord.sessionId);
17841
17732
  markTurnFailed({
17842
17733
  conversation,
17843
17734
  nowMs: Date.now(),
17844
- sessionId: checkpoint.sessionId,
17845
- userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
17735
+ sessionId: sessionRecord.sessionId,
17736
+ userMessageId: getTurnUserMessage(conversation, sessionRecord.sessionId)?.id,
17846
17737
  markConversationMessage,
17847
17738
  updateConversationStats
17848
17739
  });
17849
- await failCheckpointBestEffort3({
17850
- checkpoint,
17740
+ await failSessionRecordBestEffort3({
17741
+ sessionRecord,
17851
17742
  errorMessage: "Timed-out turn failed while resuming"
17852
17743
  });
17853
- await persistThreadStateById(checkpoint.conversationId, {
17744
+ await persistThreadStateById(sessionRecord.conversationId, {
17854
17745
  conversation
17855
17746
  });
17856
17747
  }
@@ -17867,11 +17758,11 @@ async function resumeTimedOutTurn(payload) {
17867
17758
  threadTs: thread.threadTs,
17868
17759
  lockKey: payload.conversationId,
17869
17760
  beforeStart: async () => {
17870
- const checkpoint = await getAgentTurnSessionCheckpoint(
17761
+ const sessionRecord = await getAgentTurnSessionRecord(
17871
17762
  payload.conversationId,
17872
17763
  payload.sessionId
17873
17764
  );
17874
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
17765
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" || sessionRecord.version !== payload.expectedVersion) {
17875
17766
  return false;
17876
17767
  }
17877
17768
  const currentState = await getPersistedThreadState(
@@ -17931,16 +17822,16 @@ async function resumeTimedOutTurn(payload) {
17931
17822
  ...getTurnUserReplyAttachmentContext(userMessage2)
17932
17823
  },
17933
17824
  onSuccess: async (reply) => {
17934
- await persistCompletedReplyState2({ checkpoint, reply });
17825
+ await persistCompletedReplyState2({ sessionRecord, reply });
17935
17826
  },
17936
17827
  onFailure: async () => {
17937
- await persistFailedReplyState2(checkpoint);
17828
+ await persistFailedReplyState2(sessionRecord);
17938
17829
  },
17939
17830
  onPostDeliveryCommitFailure: async () => {
17940
- await failAgentTurnSessionCheckpoint({
17941
- conversationId: checkpoint.conversationId,
17942
- expectedCheckpointVersion: checkpoint.checkpointVersion,
17943
- sessionId: checkpoint.sessionId,
17831
+ await failAgentTurnSessionRecord({
17832
+ conversationId: sessionRecord.conversationId,
17833
+ expectedVersion: sessionRecord.version,
17834
+ sessionId: sessionRecord.sessionId,
17944
17835
  errorMessage: "Timed-out turn reply was delivered but completion state did not persist"
17945
17836
  });
17946
17837
  },
@@ -17963,11 +17854,11 @@ async function resumeTimedOutTurn(payload) {
17963
17854
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
17964
17855
  throw error;
17965
17856
  }
17966
- const checkpointVersion = error.metadata?.checkpointVersion;
17857
+ const version = error.metadata?.version;
17967
17858
  const nextSliceId = error.metadata?.sliceId;
17968
- if (typeof checkpointVersion !== "number") {
17859
+ if (typeof version !== "number") {
17969
17860
  throw new Error(
17970
- "Timed-out resume turn did not include a checkpoint version"
17861
+ "Timed-out resume turn did not include a turn-session version"
17971
17862
  );
17972
17863
  }
17973
17864
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -17988,7 +17879,7 @@ async function resumeTimedOutTurn(payload) {
17988
17879
  await scheduleTurnTimeoutResume({
17989
17880
  conversationId: payload.conversationId,
17990
17881
  sessionId: payload.sessionId,
17991
- expectedCheckpointVersion: checkpointVersion
17882
+ expectedVersion: version
17992
17883
  });
17993
17884
  }
17994
17885
  };
@@ -18383,6 +18274,26 @@ async function decideSubscribedThreadReply(args) {
18383
18274
  }
18384
18275
  }
18385
18276
 
18277
+ // src/chat/runtime/turn-input.ts
18278
+ function combineTextParts(queuedTexts, latestText) {
18279
+ const parts = [...queuedTexts, latestText].filter(
18280
+ (part) => part.trim().length > 0
18281
+ );
18282
+ return parts.length > 0 ? parts.join("\n\n") : latestText;
18283
+ }
18284
+ function combineTurnText(queuedMessages, latestText) {
18285
+ return {
18286
+ rawText: combineTextParts(
18287
+ queuedMessages.map((message) => message.rawText),
18288
+ latestText.rawText
18289
+ ),
18290
+ userText: combineTextParts(
18291
+ queuedMessages.map((message) => message.userText),
18292
+ latestText.userText
18293
+ )
18294
+ };
18295
+ }
18296
+
18386
18297
  // src/chat/runtime/slack-runtime.ts
18387
18298
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
18388
18299
  async function maybeHandleThreadOptOutDecision(args) {
@@ -18394,6 +18305,19 @@ async function maybeHandleThreadOptOutDecision(args) {
18394
18305
  await args.thread.post(THREAD_OPTOUT_ACK);
18395
18306
  return true;
18396
18307
  }
18308
+ function getQueuedMessages(context, options) {
18309
+ return (context?.skipped ?? []).map((message) => {
18310
+ const stripped = options.stripLeadingBotMention(message.text, {
18311
+ stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
18312
+ });
18313
+ return {
18314
+ explicitMention: options.explicitMention || Boolean(message.isMention),
18315
+ message,
18316
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
18317
+ userText: appendSlackLegacyAttachmentText(stripped, message.raw)
18318
+ };
18319
+ });
18320
+ }
18397
18321
  function buildLogContext(deps, args) {
18398
18322
  return {
18399
18323
  conversationId: args.threadId ?? args.runId,
@@ -18463,7 +18387,7 @@ function createSlackTurnRuntime(deps) {
18463
18387
  message: args.message,
18464
18388
  decision: args.decision,
18465
18389
  completedAtMs,
18466
- userText: args.userText
18390
+ text: args.text
18467
18391
  });
18468
18392
  }
18469
18393
  };
@@ -18493,9 +18417,14 @@ function createSlackTurnRuntime(deps) {
18493
18417
  );
18494
18418
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
18495
18419
  await thread.subscribe();
18420
+ const queuedMessages = getQueuedMessages(hooks?.messageContext, {
18421
+ explicitMention: true,
18422
+ stripLeadingBotMention: deps.stripLeadingBotMention
18423
+ });
18496
18424
  await deps.replyToThread(thread, message, {
18497
18425
  explicitMention: true,
18498
18426
  beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
18427
+ queuedMessages,
18499
18428
  onToolInvocation: toolInvocationHook
18500
18429
  });
18501
18430
  });
@@ -18558,28 +18487,33 @@ function createSlackTurnRuntime(deps) {
18558
18487
  const legacyAttachmentText = renderSlackLegacyAttachmentText(
18559
18488
  message.raw
18560
18489
  );
18561
- const rawUserText = appendSlackLegacyAttachmentText(
18562
- message.text,
18563
- message.raw
18564
- );
18565
18490
  const strippedUserText = deps.stripLeadingBotMention(message.text, {
18566
18491
  stripLeadingSlackMentionToken: Boolean(message.isMention)
18567
18492
  });
18568
- const userText = appendSlackLegacyAttachmentText(
18569
- strippedUserText,
18570
- message.raw
18571
- );
18493
+ const currentText = {
18494
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
18495
+ userText: appendSlackLegacyAttachmentText(
18496
+ strippedUserText,
18497
+ message.raw
18498
+ )
18499
+ };
18572
18500
  const threadContext = {
18573
18501
  threadId,
18574
18502
  requesterId: message.author.userId,
18575
18503
  channelId,
18576
18504
  runId
18577
18505
  };
18506
+ const queuedMessages = getQueuedMessages(hooks?.messageContext, {
18507
+ explicitMention: Boolean(message.isMention),
18508
+ stripLeadingBotMention: deps.stripLeadingBotMention
18509
+ });
18510
+ const combinedText = combineTurnText(queuedMessages, currentText);
18511
+ const turnIsExplicitMention = Boolean(message.isMention) || queuedMessages.some((queued) => queued.explicitMention);
18578
18512
  const preflightDecision = getSubscribedReplyPreflightDecision({
18579
18513
  botUserName: deps.assistantUserName,
18580
- rawText: rawUserText,
18581
- text: userText,
18582
- isExplicitMention: Boolean(message.isMention)
18514
+ rawText: combinedText.rawText,
18515
+ text: combinedText.userText,
18516
+ isExplicitMention: turnIsExplicitMention
18583
18517
  });
18584
18518
  if (preflightDecision && !preflightDecision.shouldReply) {
18585
18519
  const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
@@ -18588,27 +18522,30 @@ function createSlackTurnRuntime(deps) {
18588
18522
  message,
18589
18523
  decision: { shouldReply: false, reason },
18590
18524
  context: threadContext,
18591
- userText
18525
+ text: combinedText
18592
18526
  });
18593
18527
  return;
18594
18528
  }
18595
18529
  const preparedState = await deps.prepareTurnState({
18596
18530
  thread,
18597
18531
  message,
18598
- userText,
18532
+ text: currentText,
18599
18533
  explicitMention: Boolean(message.isMention),
18600
- context: threadContext
18534
+ context: threadContext,
18535
+ queuedMessages
18601
18536
  });
18602
18537
  await deps.persistPreparedState({
18603
18538
  thread,
18604
18539
  preparedState
18605
18540
  });
18606
18541
  const decision = await deps.decideSubscribedReply({
18607
- rawText: rawUserText,
18608
- text: userText,
18542
+ rawText: combinedText.rawText,
18543
+ text: combinedText.userText,
18609
18544
  conversationContext: deps.getPreparedConversationContext(preparedState),
18610
- hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
18611
- isExplicitMention: Boolean(message.isMention),
18545
+ hasAttachments: message.attachments.length > 0 || queuedMessages.some(
18546
+ (queued) => queued.message.attachments.length > 0
18547
+ ) || legacyAttachmentText !== "",
18548
+ isExplicitMention: turnIsExplicitMention,
18612
18549
  context: threadContext
18613
18550
  });
18614
18551
  if (await maybeHandleThreadOptOutDecision({
@@ -18622,7 +18559,7 @@ function createSlackTurnRuntime(deps) {
18622
18559
  decision,
18623
18560
  context: threadContext,
18624
18561
  preparedState,
18625
- userText
18562
+ text: combinedText
18626
18563
  });
18627
18564
  return;
18628
18565
  }
@@ -18633,7 +18570,7 @@ function createSlackTurnRuntime(deps) {
18633
18570
  decision,
18634
18571
  context: threadContext,
18635
18572
  preparedState,
18636
- userText
18573
+ text: combinedText
18637
18574
  });
18638
18575
  return;
18639
18576
  }
@@ -18651,6 +18588,7 @@ function createSlackTurnRuntime(deps) {
18651
18588
  explicitMention: Boolean(message.isMention),
18652
18589
  preparedState,
18653
18590
  beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
18591
+ queuedMessages,
18654
18592
  onToolInvocation: toolInvocationHook
18655
18593
  });
18656
18594
  });
@@ -18748,6 +18686,7 @@ function createSlackTurnRuntime(deps) {
18748
18686
  }
18749
18687
 
18750
18688
  // src/chat/services/context-compaction.ts
18689
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS6 } from "chat";
18751
18690
  import {
18752
18691
  estimateContextTokens,
18753
18692
  estimateTokens
@@ -18930,22 +18869,8 @@ function buildReplacementHistory(args) {
18930
18869
  ${args.summary}`)
18931
18870
  ];
18932
18871
  }
18933
- function createCompactionSessionId(previousSessionId) {
18934
- return `compaction_${previousSessionId}`;
18935
- }
18936
- async function loadCompactionSource(args) {
18937
- const checkpoint = await getAgentTurnSessionCheckpoint(
18938
- args.conversationId,
18939
- args.previousSessionId
18940
- );
18941
- if (!checkpoint) {
18942
- return { reason: "missing_context" };
18943
- }
18944
- if (checkpoint.state !== "completed") {
18945
- return { reason: "not_completed" };
18946
- }
18947
- const messages = checkpoint.piMessages;
18948
- if (messages.length) {
18872
+ function loadCompactionSource(messages) {
18873
+ if (messages.length > 0) {
18949
18874
  return {
18950
18875
  estimatedTokens: estimateHistoryTokens(messages),
18951
18876
  messages
@@ -18954,10 +18879,7 @@ async function loadCompactionSource(args) {
18954
18879
  return { reason: "missing_context" };
18955
18880
  }
18956
18881
  async function maybeCompactWithDeps(args, deps) {
18957
- const source = await loadCompactionSource({
18958
- conversationId: args.conversationId,
18959
- previousSessionId: args.previousSessionId
18960
- });
18882
+ const source = loadCompactionSource(args.piMessages);
18961
18883
  if ("reason" in source) {
18962
18884
  return { compacted: false, reason: source.reason };
18963
18885
  }
@@ -19004,29 +18926,22 @@ async function writeCompactedThreadContext(args, sourceMessages, summary, contex
19004
18926
  messages: trimTrailingAssistantMessages(sourceMessages),
19005
18927
  summary
19006
18928
  });
19007
- const nextSessionId = createCompactionSessionId(args.previousSessionId);
19008
- await upsertAgentTurnSessionCheckpoint({
18929
+ await commitMessages({
19009
18930
  conversationId: args.conversationId,
19010
- sessionId: nextSessionId,
19011
- sliceId: 1,
19012
- state: "completed",
19013
- piMessages: replacement
18931
+ messages: replacement,
18932
+ ttlMs: THREAD_STATE_TTL_MS6
19014
18933
  });
19015
- args.conversation.processing.lastSessionId = nextSessionId;
19016
18934
  updateConversationStats(args.conversation);
19017
18935
  setSpanAttributes({
19018
18936
  "app.compaction.input_messages": sourceMessages.length,
19019
18937
  "app.compaction.retained_messages": replacement.length - 1,
19020
18938
  "app.compaction.summary_chars": summary.length,
19021
- "app.compaction.previous_session_id": args.previousSessionId,
19022
- "app.compaction.next_session_id": nextSessionId,
19023
18939
  ...context.triggerTokens !== void 0 ? { "app.compaction.trigger_tokens": context.triggerTokens } : {},
19024
18940
  "app.context_tokens_estimated": context.estimatedTokens
19025
18941
  });
19026
18942
  return {
19027
18943
  compacted: true,
19028
- piMessages: replacement,
19029
- sessionId: nextSessionId
18944
+ piMessages: replacement
19030
18945
  };
19031
18946
  }
19032
18947
  function createContextCompactor(deps) {
@@ -19736,7 +19651,7 @@ function maybeUpdateAssistantTitle(args) {
19736
19651
  assistantThreadContext.threadTs,
19737
19652
  title
19738
19653
  );
19739
- return titleSourceMessage.id;
19654
+ return { sourceMessageId: titleSourceMessage.id, title };
19740
19655
  } catch (error) {
19741
19656
  const slackErrorCode = getSlackApiErrorCode(error);
19742
19657
  const assistantTitleErrorAttributes = {
@@ -19760,7 +19675,7 @@ function maybeUpdateAssistantTitle(args) {
19760
19675
  assistantTitleErrorAttributes,
19761
19676
  "Skipping thread title update due to Slack permission error"
19762
19677
  );
19763
- return titleSourceMessage.id;
19678
+ return { sourceMessageId: titleSourceMessage.id };
19764
19679
  }
19765
19680
  logWarn(
19766
19681
  "thread_title_generation_failed",
@@ -19814,6 +19729,27 @@ function collectCanvasUrls(artifacts) {
19814
19729
  ].filter((url) => typeof url === "string" && url !== "")
19815
19730
  );
19816
19731
  }
19732
+ function turnRequester(args) {
19733
+ const requester = {
19734
+ ...args.email ? { email: args.email } : {},
19735
+ ...args.fullName ? { fullName: args.fullName } : {},
19736
+ ...args.userId ? { slackUserId: args.userId } : {},
19737
+ ...args.userName ? { slackUserName: args.userName } : {}
19738
+ };
19739
+ return Object.keys(requester).length > 0 ? requester : void 0;
19740
+ }
19741
+ async function resolveChannelName(thread) {
19742
+ const existingName = thread.channel.name?.trim();
19743
+ if (existingName) {
19744
+ return existingName;
19745
+ }
19746
+ try {
19747
+ const metadata = await thread.channel.fetchMetadata();
19748
+ return metadata.name?.trim() || void 0;
19749
+ } catch {
19750
+ return void 0;
19751
+ }
19752
+ }
19817
19753
  function getCurrentTurnCanvasUrl(args) {
19818
19754
  const previousUrls = collectCanvasUrls(args.before);
19819
19755
  const latestUrls = collectCanvasUrls(args.after);
@@ -19827,35 +19763,37 @@ function getCurrentTurnCanvasUrl(args) {
19827
19763
  function buildCanvasRecoveryReply(canvasUrl) {
19828
19764
  return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
19829
19765
  }
19766
+ function collectTurnAttachments(message, queuedMessages) {
19767
+ return [
19768
+ ...(queuedMessages ?? []).flatMap((queued) => queued.message.attachments),
19769
+ ...message.attachments
19770
+ ];
19771
+ }
19830
19772
  async function loadPiMessagesForTurn(args) {
19831
19773
  const fallback = args.fallback.length > 0 ? [...args.fallback] : void 0;
19832
19774
  if (!args.conversationId) {
19833
19775
  return { piMessages: fallback };
19834
19776
  }
19835
19777
  if (args.activeTurnId) {
19836
- const checkpoint2 = await getAgentTurnSessionCheckpoint(
19778
+ const sessionRecord = await getAgentTurnSessionRecord(
19837
19779
  args.conversationId,
19838
19780
  args.activeTurnId
19839
19781
  );
19840
- if (checkpoint2?.piMessages.length) {
19782
+ if (sessionRecord?.piMessages.length) {
19841
19783
  return {
19842
19784
  piMessages: stripRuntimeTurnContext(
19843
- trimTrailingAssistantMessages(checkpoint2.piMessages)
19785
+ trimTrailingAssistantMessages(sessionRecord.piMessages)
19844
19786
  )
19845
19787
  };
19846
19788
  }
19847
19789
  }
19848
- if (!args.lastSessionId) {
19849
- return { piMessages: fallback };
19850
- }
19851
- const checkpoint = await getAgentTurnSessionCheckpoint(
19852
- args.conversationId,
19853
- args.lastSessionId
19854
- );
19855
- if (checkpoint?.state === "completed" && checkpoint.piMessages.length > 0) {
19790
+ const projection = await loadProjection({
19791
+ conversationId: args.conversationId
19792
+ });
19793
+ if (projection.length > 0) {
19856
19794
  return {
19857
- compactionSessionId: args.lastSessionId,
19858
- piMessages: stripRuntimeTurnContext(checkpoint.piMessages)
19795
+ canCompact: true,
19796
+ piMessages: projection
19859
19797
  };
19860
19798
  }
19861
19799
  return { piMessages: fallback };
@@ -19867,6 +19805,7 @@ function createReplyToThread(deps) {
19867
19805
  }
19868
19806
  const threadId = getThreadId(thread, message);
19869
19807
  const channelId = getChannelId(thread, message);
19808
+ const channelName = channelId ? await resolveChannelName(thread) : void 0;
19870
19809
  const threadTs = getThreadTs(threadId);
19871
19810
  const assistantThreadContext = getAssistantThreadContext(message);
19872
19811
  const messageTs = getMessageTs(message);
@@ -19889,17 +19828,25 @@ function createReplyToThread(deps) {
19889
19828
  const strippedUserText = stripLeadingBotMention(message.text, {
19890
19829
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
19891
19830
  });
19892
- const userText = appendSlackLegacyAttachmentText(
19893
- strippedUserText,
19894
- message.raw
19895
- );
19831
+ const currentText = {
19832
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
19833
+ userText: appendSlackLegacyAttachmentText(
19834
+ strippedUserText,
19835
+ message.raw
19836
+ )
19837
+ };
19838
+ const effectiveUserText = combineTurnText(
19839
+ options.queuedMessages ?? [],
19840
+ currentText
19841
+ ).userText;
19896
19842
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
19897
19843
  thread,
19898
19844
  message,
19899
- userText,
19845
+ text: currentText,
19900
19846
  explicitMention: Boolean(
19901
19847
  options.explicitMention || message.isMention
19902
19848
  ),
19849
+ queuedMessages: options.queuedMessages,
19903
19850
  context: {
19904
19851
  threadId,
19905
19852
  requesterId: message.author.userId,
@@ -19909,6 +19856,15 @@ function createReplyToThread(deps) {
19909
19856
  });
19910
19857
  const slackMessageTs = getSlackMessageTs(message);
19911
19858
  const turnId = buildDeterministicTurnId(message.id);
19859
+ const fallbackIdentity = await deps.services.lookupSlackUser(
19860
+ message.author.userId
19861
+ );
19862
+ const requester = turnRequester({
19863
+ email: fallbackIdentity?.email,
19864
+ fullName: message.author.fullName ?? fallbackIdentity?.fullName,
19865
+ userId: message.author.userId,
19866
+ userName: message.author.userName ?? fallbackIdentity?.userName
19867
+ });
19912
19868
  const turnTraceContext = {
19913
19869
  conversationId,
19914
19870
  slackThreadId: threadId,
@@ -19990,7 +19946,7 @@ function createReplyToThread(deps) {
19990
19946
  "agent_turn_continuation_retry_schedule_failed",
19991
19947
  turnTraceContext,
19992
19948
  {
19993
- "app.ai.resume_checkpoint_version": resumeRequest.expectedCheckpointVersion,
19949
+ "app.ai.resume_session_version": resumeRequest.expectedVersion,
19994
19950
  "app.ai.resume_session_id": resumeRequest.sessionId,
19995
19951
  ...messageTs ? { "messaging.message.id": messageTs } : {}
19996
19952
  },
@@ -20013,11 +19969,10 @@ function createReplyToThread(deps) {
20013
19969
  return;
20014
19970
  }
20015
19971
  }
20016
- const lastSessionIdForHistory = preparedState.conversation.processing.lastSessionId;
20017
19972
  const configReply = await maybeApplyProviderDefaultConfigRequest({
20018
19973
  channelConfiguration: preparedState.channelConfiguration,
20019
19974
  requesterId: message.author.userId,
20020
- text: userText
19975
+ text: effectiveUserText
20021
19976
  });
20022
19977
  if (configReply) {
20023
19978
  await beforeFirstResponsePost();
@@ -20053,6 +20008,28 @@ function createReplyToThread(deps) {
20053
20008
  nextTurnId: turnId,
20054
20009
  updateConversationStats
20055
20010
  });
20011
+ if (conversationId) {
20012
+ void recordAgentTurnSessionSummary({
20013
+ channelName,
20014
+ conversationId,
20015
+ sessionId: turnId,
20016
+ sliceId: 1,
20017
+ startedAtMs: message.metadata.dateSent.getTime(),
20018
+ state: "running",
20019
+ requester,
20020
+ traceId: getActiveTraceId()
20021
+ }).catch((error) => {
20022
+ logException(
20023
+ error,
20024
+ "agent_turn_summary_record_failed",
20025
+ turnTraceContext,
20026
+ {
20027
+ "app.agent.turn.state": "running"
20028
+ },
20029
+ "Failed to record running turn summary"
20030
+ );
20031
+ });
20032
+ }
20056
20033
  setTags({
20057
20034
  conversationId
20058
20035
  });
@@ -20070,15 +20047,16 @@ function createReplyToThread(deps) {
20070
20047
  await persistThreadState(thread, {
20071
20048
  conversation: preparedState.conversation
20072
20049
  });
20073
- const fallbackIdentity = await deps.services.lookupSlackUser(
20074
- message.author.userId
20075
- );
20076
20050
  const resolvedUserName = message.author.userName ?? fallbackIdentity?.userName;
20077
20051
  if (resolvedUserName) {
20078
20052
  setTags({ slackUserName: resolvedUserName });
20079
20053
  }
20054
+ const turnAttachments = collectTurnAttachments(
20055
+ message,
20056
+ options.queuedMessages
20057
+ );
20080
20058
  const userAttachments = await deps.resolveUserAttachments(
20081
- message.attachments,
20059
+ turnAttachments,
20082
20060
  {
20083
20061
  threadId,
20084
20062
  requesterId: message.author.userId,
@@ -20088,7 +20066,7 @@ function createReplyToThread(deps) {
20088
20066
  messageTs: slackMessageTs
20089
20067
  }
20090
20068
  );
20091
- const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
20069
+ const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(turnAttachments) ? countPotentialImageAttachments(turnAttachments) : 0;
20092
20070
  const status = createSlackAdapterAssistantStatusSession({
20093
20071
  channelId: assistantThreadContext?.channelId,
20094
20072
  threadTs: assistantThreadContext?.threadTs,
@@ -20123,11 +20101,10 @@ function createReplyToThread(deps) {
20123
20101
  const loadedPiMessages = await loadPiMessagesForTurn({
20124
20102
  conversationId,
20125
20103
  activeTurnId,
20126
- lastSessionId: lastSessionIdForHistory,
20127
20104
  fallback: preparedState.conversation.piMessages
20128
20105
  });
20129
20106
  let piMessages = loadedPiMessages.piMessages;
20130
- if (conversationId && loadedPiMessages.compactionSessionId && piMessages?.length) {
20107
+ if (conversationId && loadedPiMessages.canCompact && piMessages?.length) {
20131
20108
  const compaction = await deps.services.contextCompactor.maybeCompact({
20132
20109
  conversation: preparedState.conversation,
20133
20110
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
@@ -20139,7 +20116,7 @@ function createReplyToThread(deps) {
20139
20116
  runId
20140
20117
  },
20141
20118
  onCompactionStart: () => status.start(compactingStatus),
20142
- previousSessionId: loadedPiMessages.compactionSessionId
20119
+ piMessages
20143
20120
  });
20144
20121
  if (compaction.compacted) {
20145
20122
  piMessages = compaction.piMessages;
@@ -20163,61 +20140,65 @@ function createReplyToThread(deps) {
20163
20140
  threadId
20164
20141
  });
20165
20142
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
20166
- let reply = await deps.services.generateAssistantReply(userText, {
20167
- requester: {
20168
- userId: message.author.userId,
20169
- userName: message.author.userName ?? fallbackIdentity?.userName,
20170
- fullName: message.author.fullName ?? fallbackIdentity?.fullName,
20171
- email: fallbackIdentity?.email
20172
- },
20173
- conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
20174
- artifactState: preparedState.artifacts,
20175
- piMessages,
20176
- pendingAuth: preparedState.conversation.processing.pendingAuth,
20177
- configuration: preparedState.configuration,
20178
- channelConfiguration: preparedState.channelConfiguration,
20179
- inboundAttachmentCount: message.attachments.length,
20180
- omittedImageAttachmentCount,
20181
- userAttachments,
20182
- correlation: {
20183
- conversationId,
20184
- threadId,
20185
- turnId,
20186
- threadTs,
20187
- messageTs,
20188
- teamId,
20189
- runId,
20190
- channelId,
20191
- requesterId: message.author.userId
20192
- },
20193
- toolChannelId,
20194
- sandbox: {
20195
- sandboxId: preparedState.sandboxId,
20196
- sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
20197
- },
20198
- onSandboxAcquired: async (sandbox) => {
20199
- await persistThreadState(thread, {
20200
- sandboxId: sandbox.sandboxId,
20201
- sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
20202
- });
20203
- },
20204
- onArtifactStateUpdated: async (artifacts) => {
20205
- latestArtifacts = artifacts;
20206
- await persistThreadState(thread, { artifacts });
20207
- },
20208
- onAuthPending: async (pendingAuth) => {
20209
- await applyPendingAuthUpdate({
20210
- conversation: preparedState.conversation,
20143
+ let reply = await deps.services.generateAssistantReply(
20144
+ effectiveUserText,
20145
+ {
20146
+ requester: {
20147
+ userId: message.author.userId,
20148
+ userName: message.author.userName ?? fallbackIdentity?.userName,
20149
+ fullName: message.author.fullName ?? fallbackIdentity?.fullName,
20150
+ email: fallbackIdentity?.email
20151
+ },
20152
+ conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
20153
+ artifactState: preparedState.artifacts,
20154
+ piMessages,
20155
+ pendingAuth: preparedState.conversation.processing.pendingAuth,
20156
+ configuration: preparedState.configuration,
20157
+ channelConfiguration: preparedState.channelConfiguration,
20158
+ inboundAttachmentCount: turnAttachments.length,
20159
+ omittedImageAttachmentCount,
20160
+ userAttachments,
20161
+ correlation: {
20211
20162
  conversationId,
20212
- nextPendingAuth: pendingAuth
20213
- });
20214
- await persistThreadState(thread, {
20215
- conversation: preparedState.conversation
20216
- });
20217
- },
20218
- onStatus: (nextStatus) => status.update(nextStatus),
20219
- onToolInvocation: options.onToolInvocation
20220
- });
20163
+ threadId,
20164
+ turnId,
20165
+ threadTs,
20166
+ messageTs,
20167
+ teamId,
20168
+ runId,
20169
+ channelId,
20170
+ channelName,
20171
+ requesterId: message.author.userId
20172
+ },
20173
+ toolChannelId,
20174
+ sandbox: {
20175
+ sandboxId: preparedState.sandboxId,
20176
+ sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
20177
+ },
20178
+ onSandboxAcquired: async (sandbox) => {
20179
+ await persistThreadState(thread, {
20180
+ sandboxId: sandbox.sandboxId,
20181
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
20182
+ });
20183
+ },
20184
+ onArtifactStateUpdated: async (artifacts) => {
20185
+ latestArtifacts = artifacts;
20186
+ await persistThreadState(thread, { artifacts });
20187
+ },
20188
+ onAuthPending: async (pendingAuth) => {
20189
+ await applyPendingAuthUpdate({
20190
+ conversation: preparedState.conversation,
20191
+ conversationId,
20192
+ nextPendingAuth: pendingAuth
20193
+ });
20194
+ await persistThreadState(thread, {
20195
+ conversation: preparedState.conversation
20196
+ });
20197
+ },
20198
+ onStatus: (nextStatus) => status.update(nextStatus),
20199
+ onToolInvocation: options.onToolInvocation
20200
+ }
20201
+ );
20221
20202
  const diagnosticsContext = {
20222
20203
  slackThreadId: threadId,
20223
20204
  slackUserId: message.author.userId,
@@ -20305,7 +20286,10 @@ function createReplyToThread(deps) {
20305
20286
  }
20306
20287
  const titleUpdateResult = await assistantTitleTask;
20307
20288
  if (titleUpdateResult) {
20308
- artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
20289
+ artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult.sourceMessageId;
20290
+ if (titleUpdateResult.title) {
20291
+ artifactStatePatch.assistantTitle = titleUpdateResult.title;
20292
+ }
20309
20293
  }
20310
20294
  const completedState = buildDeliveredTurnStatePatch({
20311
20295
  artifactStatePatch,
@@ -20318,6 +20302,21 @@ function createReplyToThread(deps) {
20318
20302
  await persistThreadState(thread, {
20319
20303
  ...completedState
20320
20304
  });
20305
+ if (conversationId) {
20306
+ await recordAgentTurnSessionSummary({
20307
+ channelName,
20308
+ conversationId,
20309
+ cumulativeDurationMs: reply.diagnostics.durationMs,
20310
+ cumulativeUsage: reply.diagnostics.usage,
20311
+ sessionId: turnId,
20312
+ sliceId: 1,
20313
+ startedAtMs: message.metadata.dateSent.getTime(),
20314
+ state: "completed",
20315
+ conversationTitle: titleUpdateResult?.title,
20316
+ requester,
20317
+ traceId: getActiveTraceId()
20318
+ });
20319
+ }
20321
20320
  preparedState.conversation = completedState.conversation;
20322
20321
  persistedAtLeastOnce = true;
20323
20322
  if (shouldEmitDevAgentTrace()) {
@@ -20349,14 +20348,14 @@ function createReplyToThread(deps) {
20349
20348
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
20350
20349
  const conversationIdForResume = error.metadata?.conversationId;
20351
20350
  const sessionIdForResume = error.metadata?.sessionId;
20352
- const checkpointVersion = error.metadata?.checkpointVersion;
20351
+ const version = error.metadata?.version;
20353
20352
  const nextSliceId = error.metadata?.sliceId;
20354
- if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
20353
+ if (conversationIdForResume && sessionIdForResume && typeof version === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
20355
20354
  try {
20356
20355
  await deps.services.scheduleTurnTimeoutResume({
20357
20356
  conversationId: conversationIdForResume,
20358
20357
  sessionId: sessionIdForResume,
20359
- expectedCheckpointVersion: checkpointVersion
20358
+ expectedVersion: version
20360
20359
  });
20361
20360
  shouldPersistFailureState = false;
20362
20361
  } catch (scheduleError) {
@@ -20366,7 +20365,7 @@ function createReplyToThread(deps) {
20366
20365
  turnTraceContext,
20367
20366
  {
20368
20367
  ...messageTs ? { "messaging.message.id": messageTs } : {},
20369
- "app.ai.resume_checkpoint_version": checkpointVersion
20368
+ "app.ai.resume_session_version": version
20370
20369
  },
20371
20370
  "Failed to schedule timeout resume callback"
20372
20371
  );
@@ -20375,7 +20374,7 @@ function createReplyToThread(deps) {
20375
20374
  }
20376
20375
  await postTurnContinuationNotice();
20377
20376
  return;
20378
- } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
20377
+ } else if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
20379
20378
  logWarn(
20380
20379
  "agent_turn_timeout_resume_slice_limit_reached",
20381
20380
  turnTraceContext,
@@ -20465,18 +20464,35 @@ function createReplyToThread(deps) {
20465
20464
  });
20466
20465
  if (conversationId) {
20467
20466
  try {
20468
- await failAgentTurnSessionCheckpoint({
20467
+ await recordAgentTurnSessionSummary({
20468
+ channelName,
20469
20469
  conversationId,
20470
20470
  sessionId: turnId,
20471
- errorMessage: "Agent turn failed before final reply delivery completed"
20471
+ sliceId: 1,
20472
+ startedAtMs: message.metadata.dateSent.getTime(),
20473
+ state: "failed",
20474
+ requester,
20475
+ traceId: getActiveTraceId()
20472
20476
  });
20473
- } catch (checkpointError) {
20477
+ const sessionRecord = await getAgentTurnSessionRecord(
20478
+ conversationId,
20479
+ turnId
20480
+ );
20481
+ if (sessionRecord) {
20482
+ await failAgentTurnSessionRecord({
20483
+ conversationId,
20484
+ expectedVersion: sessionRecord.version,
20485
+ sessionId: turnId,
20486
+ errorMessage: "Agent turn failed before final reply delivery completed"
20487
+ });
20488
+ }
20489
+ } catch (recordError) {
20474
20490
  logException(
20475
- checkpointError,
20476
- "agent_turn_failed_checkpoint_persist_failed",
20491
+ recordError,
20492
+ "agent_turn_failed_session_record_persist_failed",
20477
20493
  turnTraceContext,
20478
20494
  {},
20479
- "Failed to mark failed turn checkpoint"
20495
+ "Failed to mark failed turn session record"
20480
20496
  );
20481
20497
  }
20482
20498
  }
@@ -20533,35 +20549,51 @@ async function refreshAssistantThreadContext(event) {
20533
20549
  await syncAssistantThreadContext(event, { setInitialTitle: false });
20534
20550
  }
20535
20551
 
20536
- // src/chat/runtime/turn-preparation.ts
20537
- var BACKFILL_MESSAGE_LIMIT = 80;
20538
- function hasPendingImageHydration(conversation) {
20539
- return conversation.messages.some(
20540
- (message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
20552
+ // src/chat/runtime/conversation-message.ts
20553
+ var NON_TEXT_MESSAGE_TEXT = "[non-text message]";
20554
+ function resolveMessageText(args) {
20555
+ const text = normalizeConversationText(args.text);
20556
+ return text || NON_TEXT_MESSAGE_TEXT;
20557
+ }
20558
+ function toConversationMessage(args) {
20559
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
20560
+ args.entry.attachments
20541
20561
  );
20542
- }
20543
- function createConversationMessageFromSdkMessage(entry) {
20544
- const enrichedText = appendSlackLegacyAttachmentText(entry.text, entry.raw);
20545
- const rawText = normalizeConversationText(enrichedText);
20546
- if (!rawText) {
20547
- return null;
20548
- }
20562
+ const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.entry.attachments) : 0;
20549
20563
  return {
20550
- id: entry.id,
20551
- role: entry.author.isMe ? "assistant" : "user",
20552
- text: rawText,
20553
- createdAtMs: entry.metadata.dateSent.getTime(),
20564
+ id: args.entry.id,
20565
+ role: args.entry.author.isMe ? "assistant" : "user",
20566
+ text: resolveMessageText(args),
20567
+ createdAtMs: args.entry.metadata.dateSent.getTime(),
20554
20568
  author: {
20555
- userId: entry.author.userId,
20556
- userName: entry.author.userName,
20557
- fullName: entry.author.fullName,
20558
- isBot: typeof entry.author.isBot === "boolean" ? entry.author.isBot : void 0
20569
+ userId: args.entry.author.userId,
20570
+ userName: args.entry.author.userName,
20571
+ fullName: args.entry.author.fullName,
20572
+ isBot: typeof args.entry.author.isBot === "boolean" ? args.entry.author.isBot : void 0
20559
20573
  },
20560
20574
  meta: {
20561
- slackTs: getSlackMessageTs(entry)
20575
+ attachmentCount: args.entry.attachments.length,
20576
+ explicitMention: args.explicitMention,
20577
+ imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
20578
+ imagesHydrated: !messageHasPotentialImageAttachment,
20579
+ slackTs: getSlackMessageTs(args.entry)
20562
20580
  }
20563
20581
  };
20564
20582
  }
20583
+
20584
+ // src/chat/runtime/turn-preparation.ts
20585
+ var BACKFILL_MESSAGE_LIMIT = 80;
20586
+ function hasPendingImageHydration(conversation) {
20587
+ return conversation.messages.some(
20588
+ (message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
20589
+ );
20590
+ }
20591
+ function getBackfillText(entry) {
20592
+ const text = normalizeConversationText(
20593
+ appendSlackLegacyAttachmentText(entry.text, entry.raw)
20594
+ );
20595
+ return text || void 0;
20596
+ }
20565
20597
  async function seedConversationBackfill(thread, conversation, currentTurn) {
20566
20598
  if (conversation.backfill.completedAtMs) {
20567
20599
  return;
@@ -20586,9 +20618,9 @@ async function seedConversationBackfill(thread, conversation, currentTurn) {
20586
20618
  }
20587
20619
  fetchedNewestFirst.reverse();
20588
20620
  for (const entry of fetchedNewestFirst) {
20589
- const message = createConversationMessageFromSdkMessage(entry);
20590
- if (message) {
20591
- seeded.push(message);
20621
+ const text = getBackfillText(entry);
20622
+ if (text) {
20623
+ seeded.push(toConversationMessage({ entry, text }));
20592
20624
  }
20593
20625
  }
20594
20626
  if (seeded.length > 0) {
@@ -20603,9 +20635,9 @@ async function seedConversationBackfill(thread, conversation, currentTurn) {
20603
20635
  }
20604
20636
  const fromRecent = thread.recentMessages.slice(-BACKFILL_MESSAGE_LIMIT);
20605
20637
  for (const entry of fromRecent) {
20606
- const message = createConversationMessageFromSdkMessage(entry);
20607
- if (message) {
20608
- seeded.push(message);
20638
+ const text = getBackfillText(entry);
20639
+ if (text) {
20640
+ seeded.push(toConversationMessage({ entry, text }));
20609
20641
  }
20610
20642
  }
20611
20643
  source = "recent_messages";
@@ -20642,35 +20674,26 @@ function createPrepareTurnState(deps) {
20642
20674
  messageId: args.message.id,
20643
20675
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
20644
20676
  });
20645
- const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
20646
- args.message.attachments
20647
- );
20648
- const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.message.attachments) : 0;
20649
- const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
20650
- const slackTs = getSlackMessageTs(args.message);
20651
- const incomingUserMessage = {
20652
- id: args.message.id,
20653
- role: "user",
20654
- text: normalizedUserText,
20655
- createdAtMs: args.message.metadata.dateSent.getTime(),
20656
- author: {
20657
- userId: args.message.author.userId,
20658
- userName: args.message.author.userName,
20659
- fullName: args.message.author.fullName,
20660
- isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
20661
- },
20662
- meta: {
20663
- attachmentCount: args.message.attachments.length,
20664
- explicitMention: args.explicitMention,
20665
- imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
20666
- slackTs,
20667
- imagesHydrated: !messageHasPotentialImageAttachment
20668
- }
20669
- };
20677
+ for (const queued of args.queuedMessages ?? []) {
20678
+ const queuedMessage = toConversationMessage({
20679
+ entry: queued.message,
20680
+ explicitMention: queued.explicitMention,
20681
+ text: queued.userText
20682
+ });
20683
+ upsertConversationMessage(conversation, queuedMessage);
20684
+ }
20685
+ const incomingUserMessage = toConversationMessage({
20686
+ entry: args.message,
20687
+ explicitMention: args.explicitMention,
20688
+ text: args.text.userText
20689
+ });
20670
20690
  const userMessageId = upsertConversationMessage(
20671
20691
  conversation,
20672
20692
  incomingUserMessage
20673
20693
  );
20694
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(args.message.attachments) || (args.queuedMessages ?? []).some(
20695
+ (queued) => hasPotentialImageAttachment(queued.message.attachments)
20696
+ );
20674
20697
  const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
20675
20698
  if (isVisionEnabled() && shouldHydrateVisionContext) {
20676
20699
  await deps.hydrateConversationVisionContext(conversation, {
@@ -20757,28 +20780,20 @@ function createSlackRuntime(options) {
20757
20780
  message,
20758
20781
  decision,
20759
20782
  completedAtMs,
20760
- userText
20783
+ text
20761
20784
  }) => {
20762
20785
  const conversation = coerceThreadConversationState(await thread.state);
20763
- const normalizedUserText = normalizeConversationText(userText) || "[non-text message]";
20764
- const slackTs = getSlackMessageTs(message);
20786
+ const conversationMessage = toConversationMessage({
20787
+ entry: message,
20788
+ explicitMention: Boolean(message.isMention),
20789
+ text: text.userText
20790
+ });
20765
20791
  upsertConversationMessage(conversation, {
20766
- id: message.id,
20767
- role: "user",
20768
- text: normalizedUserText,
20769
- createdAtMs: message.metadata.dateSent.getTime(),
20770
- author: {
20771
- userId: message.author.userId,
20772
- userName: message.author.userName,
20773
- fullName: message.author.fullName,
20774
- isBot: typeof message.author.isBot === "boolean" ? message.author.isBot : void 0
20775
- },
20792
+ ...conversationMessage,
20776
20793
  meta: {
20777
- explicitMention: Boolean(message.isMention),
20778
- slackTs,
20794
+ ...conversationMessage.meta,
20779
20795
  replied: false,
20780
- skippedReason: decision.reason,
20781
- imagesHydrated: !hasPotentialImageAttachment(message.attachments)
20796
+ skippedReason: decision.reason
20782
20797
  }
20783
20798
  });
20784
20799
  conversation.processing.activeTurnId = void 0;
@@ -21221,17 +21236,26 @@ function createProductionBot() {
21221
21236
  });
21222
21237
  }
21223
21238
  function registerProductionHandlers(bot, slackRuntime) {
21224
- bot.onNewMention((thread, message) => {
21239
+ bot.onNewMention((thread, message, context) => {
21225
21240
  rehydrateAttachmentFetchers(message);
21226
- return slackRuntime.handleNewMention(thread, message);
21241
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21242
+ return slackRuntime.handleNewMention(thread, message, {
21243
+ messageContext: context
21244
+ });
21227
21245
  });
21228
- bot.onDirectMessage((thread, message) => {
21246
+ bot.onDirectMessage((thread, message, _channel, context) => {
21229
21247
  rehydrateAttachmentFetchers(message);
21230
- return slackRuntime.handleNewMention(thread, message);
21248
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21249
+ return slackRuntime.handleNewMention(thread, message, {
21250
+ messageContext: context
21251
+ });
21231
21252
  });
21232
- bot.onSubscribedMessage((thread, message) => {
21253
+ bot.onSubscribedMessage((thread, message, context) => {
21233
21254
  rehydrateAttachmentFetchers(message);
21234
- return slackRuntime.handleSubscribedMessage(thread, message);
21255
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21256
+ return slackRuntime.handleSubscribedMessage(thread, message, {
21257
+ messageContext: context
21258
+ });
21235
21259
  });
21236
21260
  bot.onAssistantThreadStarted(
21237
21261
  (event) => slackRuntime.handleAssistantThreadStarted(event)
@@ -21718,14 +21742,13 @@ async function createApp(options) {
21718
21742
  }
21719
21743
  await next();
21720
21744
  });
21721
- app.get("/", () => GET3());
21722
- app.get("/health", () => GET2());
21723
- app.get("/api/info", () => GET());
21745
+ app.get("/", () => GET());
21746
+ app.get("/health", () => GET());
21724
21747
  app.get("/api/oauth/callback/mcp/:provider", (c) => {
21725
- return GET5(c.req.raw, c.req.param("provider"), waitUntil);
21748
+ return GET3(c.req.raw, c.req.param("provider"), waitUntil);
21726
21749
  });
21727
21750
  app.get("/api/oauth/callback/:provider", (c) => {
21728
- return GET6(c.req.raw, c.req.param("provider"), waitUntil);
21751
+ return GET4(c.req.raw, c.req.param("provider"), waitUntil);
21729
21752
  });
21730
21753
  app.post("/api/internal/turn-resume", (c) => {
21731
21754
  return POST2(c.req.raw, waitUntil);
@@ -21734,7 +21757,7 @@ async function createApp(options) {
21734
21757
  return POST(c.req.raw, waitUntil);
21735
21758
  });
21736
21759
  app.get("/api/internal/heartbeat", (c) => {
21737
- return GET4(c.req.raw, waitUntil);
21760
+ return GET2(c.req.raw, waitUntil);
21738
21761
  });
21739
21762
  app.post("/api/webhooks/:platform", (c) => {
21740
21763
  return POST3(c.req.raw, c.req.param("platform"), waitUntil);