@sentry/junior 0.57.0 → 0.59.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 (55) hide show
  1. package/dist/app.js +1311 -1284
  2. package/dist/chat/agent-dispatch/store.d.ts +4 -2
  3. package/dist/chat/agent-dispatch/types.d.ts +0 -1
  4. package/dist/chat/conversation-privacy.d.ts +23 -0
  5. package/dist/chat/logging.d.ts +2 -0
  6. package/dist/chat/mcp/tool-manager.d.ts +18 -5
  7. package/dist/chat/mcp/tool-name.d.ts +2 -0
  8. package/dist/chat/pi/client.d.ts +2 -0
  9. package/dist/chat/pi/derived-state.d.ts +5 -0
  10. package/dist/chat/pi/traced-stream.d.ts +5 -1
  11. package/dist/chat/prompt.d.ts +3 -9
  12. package/dist/chat/respond-helpers.d.ts +5 -3
  13. package/dist/chat/respond.d.ts +1 -0
  14. package/dist/chat/runtime/conversation-message.d.ts +10 -0
  15. package/dist/chat/runtime/processing-reaction.d.ts +2 -4
  16. package/dist/chat/runtime/reply-executor.d.ts +13 -16
  17. package/dist/chat/runtime/slack-runtime.d.ts +19 -32
  18. package/dist/chat/runtime/thread-state.d.ts +1 -1
  19. package/dist/chat/runtime/turn-input.d.ts +29 -0
  20. package/dist/chat/runtime/turn-preparation.d.ts +4 -24
  21. package/dist/chat/runtime/turn.d.ts +2 -3
  22. package/dist/chat/sentry-links.d.ts +4 -0
  23. package/dist/chat/services/context-compaction.d.ts +3 -4
  24. package/dist/chat/services/pending-auth.d.ts +1 -1
  25. package/dist/chat/services/subscribed-reply-policy.d.ts +2 -13
  26. package/dist/chat/services/timeout-resume.d.ts +1 -2
  27. package/dist/chat/services/turn-session-record.d.ts +82 -0
  28. package/dist/chat/slack/assistant-thread/title.d.ts +4 -1
  29. package/dist/chat/state/artifacts.d.ts +1 -0
  30. package/dist/chat/state/conversation.d.ts +0 -1
  31. package/dist/chat/state/session-log.d.ts +117 -0
  32. package/dist/chat/state/ttl.d.ts +2 -0
  33. package/dist/chat/state/turn-session.d.ts +89 -0
  34. package/dist/chat/tools/advisor/tool.d.ts +2 -0
  35. package/dist/chat/tools/agent-tools.d.ts +2 -1
  36. package/dist/chat/tools/skill/call-mcp-tool.d.ts +7 -3
  37. package/dist/chat/tools/skill/search-mcp-tools.d.ts +15 -3
  38. package/dist/chat/tools/types.d.ts +0 -1
  39. package/dist/{chunk-AA5TIFN5.js → chunk-FKEKRBUB.js} +267 -735
  40. package/dist/{chunk-TTUY467K.js → chunk-H652GMDH.js} +30 -14
  41. package/dist/chunk-I4FDGMFI.js +950 -0
  42. package/dist/{chunk-D3G3YOU4.js → chunk-ITOW4DED.js} +1 -1
  43. package/dist/chunk-QDGD5WVN.js +708 -0
  44. package/dist/cli/check.js +2 -2
  45. package/dist/cli/init.js +0 -1
  46. package/dist/cli/snapshot-warmup.js +5 -3
  47. package/dist/instrumentation.js +3 -0
  48. package/dist/reporting.d.ts +113 -0
  49. package/dist/reporting.js +390 -0
  50. package/package.json +25 -11
  51. package/dist/chat/services/turn-checkpoint.d.ts +0 -74
  52. package/dist/chat/state/pi-session-message-store.d.ts +0 -15
  53. package/dist/chat/state/turn-session-store.d.ts +0 -49
  54. package/dist/handlers/diagnostics-dashboard.d.ts +0 -2
  55. 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;
@@ -14359,9 +14248,12 @@ function buildDispatchId(plugin, idempotencyKey) {
14359
14248
  const digest = createHash2("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32);
14360
14249
  return `dispatch_${digest}`;
14361
14250
  }
14362
- function getDispatchConversationId(destination) {
14251
+ function getDispatchDestinationLockId(destination) {
14363
14252
  return `slack:${destination.teamId}:${destination.channelId}`;
14364
14253
  }
14254
+ function getDispatchConversationId(dispatch) {
14255
+ return `agent-dispatch:${dispatch.id}`;
14256
+ }
14365
14257
  function getDispatchTurnId(dispatchId) {
14366
14258
  return `dispatch:${dispatchId}`;
14367
14259
  }
@@ -14419,7 +14311,7 @@ async function syncIncompleteDispatchIndex(state, record) {
14419
14311
  await state.set(
14420
14312
  incompleteDispatchIndexKey(),
14421
14313
  next.slice(-DISPATCH_INDEX_MAX_LENGTH),
14422
- THREAD_STATE_TTL_MS4
14314
+ JUNIOR_THREAD_STATE_TTL_MS
14423
14315
  );
14424
14316
  });
14425
14317
  }
@@ -14427,7 +14319,7 @@ async function putRecord(state, record) {
14427
14319
  await state.set(
14428
14320
  getDispatchStorageKey(record.id),
14429
14321
  record,
14430
- THREAD_STATE_TTL_MS4
14322
+ JUNIOR_THREAD_STATE_TTL_MS
14431
14323
  );
14432
14324
  await syncIncompleteDispatchIndex(state, record);
14433
14325
  }
@@ -14541,7 +14433,6 @@ async function markDispatch(args) {
14541
14433
  ...current,
14542
14434
  status: args.status,
14543
14435
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
14544
- ...typeof args.resumeCheckpointVersion === "number" ? { resumeCheckpointVersion: args.resumeCheckpointVersion } : {},
14545
14436
  ...args.resultMessageTs ? { resultMessageTs: args.resultMessageTs } : {}
14546
14437
  });
14547
14438
  });
@@ -14578,14 +14469,15 @@ async function runAgentDispatchSlice(callback, deps = {}) {
14578
14469
  return;
14579
14470
  }
14580
14471
  let dispatch = claimedDispatch;
14581
- const conversationId = getDispatchConversationId(dispatch.destination);
14472
+ const conversationId = getDispatchConversationId(dispatch);
14473
+ const destinationLockId = getDispatchDestinationLockId(dispatch.destination);
14582
14474
  const stateAdapter = getStateAdapter();
14583
14475
  await stateAdapter.connect();
14584
- const conversationLock = await stateAdapter.acquireLock(
14585
- conversationId,
14476
+ const destinationLock = await stateAdapter.acquireLock(
14477
+ destinationLockId,
14586
14478
  DISPATCH_SLICE_LEASE_MS
14587
14479
  );
14588
- if (!conversationLock) {
14480
+ if (!destinationLock) {
14589
14481
  await markDispatch({
14590
14482
  dispatch,
14591
14483
  status: "pending",
@@ -14774,12 +14666,11 @@ async function runAgentDispatchSlice(callback, deps = {}) {
14774
14666
  return;
14775
14667
  }
14776
14668
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
14777
- const checkpointVersion = error.metadata?.checkpointVersion;
14669
+ const version = error.metadata?.version;
14778
14670
  const nextSliceId = error.metadata?.sliceId;
14779
- if (typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14671
+ if (typeof version === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14780
14672
  const awaiting = await markDispatch({
14781
14673
  dispatch,
14782
- resumeCheckpointVersion: checkpointVersion,
14783
14674
  status: "awaiting_resume"
14784
14675
  });
14785
14676
  await scheduleCallback({
@@ -14811,7 +14702,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
14811
14702
  errorMessage: error instanceof Error ? error.message : String(error)
14812
14703
  });
14813
14704
  } finally {
14814
- await stateAdapter.releaseLock(conversationLock);
14705
+ await stateAdapter.releaseLock(destinationLock);
14815
14706
  }
14816
14707
  }
14817
14708
 
@@ -15125,7 +15016,7 @@ function verifyHeartbeatRequest(request) {
15125
15016
  const expected = Buffer.from(secret);
15126
15017
  return actual.length === expected.length && timingSafeEqual4(actual, expected);
15127
15018
  }
15128
- async function GET4(request, waitUntil) {
15019
+ async function GET2(request, waitUntil) {
15129
15020
  if (!verifyHeartbeatRequest(request)) {
15130
15021
  return new Response("Unauthorized", { status: 401 });
15131
15022
  }
@@ -15144,6 +15035,9 @@ async function GET4(request, waitUntil) {
15144
15035
  return new Response("Accepted", { status: 202 });
15145
15036
  }
15146
15037
 
15038
+ // src/handlers/mcp-oauth-callback.ts
15039
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
15040
+
15147
15041
  // src/chat/runtime/delivered-turn-state.ts
15148
15042
  function buildDeliveredTurnStatePatch(args) {
15149
15043
  const conversation = structuredClone(args.conversation);
@@ -15490,7 +15384,7 @@ function createSlackAdapterStatusSender(args) {
15490
15384
  };
15491
15385
  }
15492
15386
  function createSlackWebApiStatusSender(args) {
15493
- const getClient3 = args.getSlackClient ?? getSlackClient;
15387
+ const getClient2 = args.getSlackClient ?? getSlackClient;
15494
15388
  return async (text, loadingMessages) => {
15495
15389
  const channelId = args.channelId;
15496
15390
  const threadTs = args.threadTs;
@@ -15503,7 +15397,7 @@ function createSlackWebApiStatusSender(args) {
15503
15397
  }
15504
15398
  const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
15505
15399
  try {
15506
- await getClient3().assistant.threads.setStatus({
15400
+ await getClient2().assistant.threads.setStatus({
15507
15401
  channel_id: normalizedChannelId,
15508
15402
  thread_ts: threadTs,
15509
15403
  status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
@@ -15649,52 +15543,6 @@ function getWorkspaceTeamId() {
15649
15543
  return workspaceTeamIdStorage.getStore();
15650
15544
  }
15651
15545
 
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
15546
  // src/chat/runtime/thread-context.ts
15699
15547
  function toSlackTeamId(value) {
15700
15548
  const candidate = toOptionalString(value);
@@ -16070,7 +15918,7 @@ async function resumeSlackTurn(args) {
16070
15918
  status.start();
16071
15919
  const generateReply = runArgs.generateReply ?? generateAssistantReply;
16072
15920
  const replyContext = createResumeReplyContext(runArgs, status);
16073
- const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
15921
+ const priorSessionRecord = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionRecord(
16074
15922
  replyContext.correlation.conversationId,
16075
15923
  replyContext.correlation.turnId
16076
15924
  ) : void 0;
@@ -16097,10 +15945,10 @@ async function resumeSlackTurn(args) {
16097
15945
  await status.stop();
16098
15946
  const footer = buildSlackReplyFooter({
16099
15947
  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,
15948
+ durationMs: typeof priorSessionRecord?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorSessionRecord?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
16101
15949
  thinkingLevel: reply.diagnostics.thinkingLevel,
16102
15950
  usage: addAgentTurnUsage(
16103
- priorCheckpoint?.cumulativeUsage,
15951
+ priorSessionRecord?.cumulativeUsage,
16104
15952
  reply.diagnostics.usage
16105
15953
  ) ?? reply.diagnostics.usage
16106
15954
  });
@@ -16291,6 +16139,9 @@ var CALLBACK_PAGES = {
16291
16139
  status: 500
16292
16140
  }
16293
16141
  };
16142
+ function mcpAuthorizationId(args) {
16143
+ return `${args.sessionId}:mcp:${args.provider}`;
16144
+ }
16294
16145
  function htmlResponse(kind) {
16295
16146
  const page = CALLBACK_PAGES[kind];
16296
16147
  return htmlCallbackResponse(page.title, page.message, page.status);
@@ -16312,27 +16163,28 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
16312
16163
  ...statePatch
16313
16164
  });
16314
16165
  }
16315
- async function failCheckpointBestEffort(args) {
16166
+ async function failSessionRecordBestEffort(args) {
16316
16167
  try {
16317
- await failAgentTurnSessionCheckpoint({
16168
+ await failAgentTurnSessionRecord({
16318
16169
  conversationId: args.conversationId,
16319
16170
  sessionId: args.sessionId,
16320
- errorMessage: args.errorMessage
16171
+ errorMessage: args.errorMessage,
16172
+ expectedVersion: args.expectedVersion
16321
16173
  });
16322
16174
  } catch (error) {
16323
16175
  logException(
16324
16176
  error,
16325
- "mcp_oauth_callback_checkpoint_fail_persist_failed",
16177
+ "mcp_oauth_callback_session_record_fail_persist_failed",
16326
16178
  {},
16327
16179
  {
16328
16180
  "app.ai.conversation_id": args.conversationId,
16329
16181
  "app.ai.session_id": args.sessionId
16330
16182
  },
16331
- "Failed to mark MCP OAuth-resumed turn checkpoint failed"
16183
+ "Failed to mark MCP OAuth-resumed turn session record failed"
16332
16184
  );
16333
16185
  }
16334
16186
  }
16335
- async function persistFailedReplyState(channelId, threadTs, sessionId) {
16187
+ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedVersion) {
16336
16188
  const threadId = `slack:${channelId}:${threadTs}`;
16337
16189
  const currentState = await getPersistedThreadState(threadId);
16338
16190
  const conversation = coerceThreadConversationState(currentState);
@@ -16345,10 +16197,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
16345
16197
  markConversationMessage,
16346
16198
  updateConversationStats
16347
16199
  });
16348
- await failCheckpointBestEffort({
16200
+ await failSessionRecordBestEffort({
16349
16201
  conversationId: threadId,
16350
16202
  sessionId,
16351
- errorMessage: "OAuth-resumed MCP turn failed"
16203
+ errorMessage: "OAuth-resumed MCP turn failed",
16204
+ expectedVersion
16352
16205
  });
16353
16206
  await persistThreadStateById(threadId, {
16354
16207
  conversation
@@ -16374,10 +16227,10 @@ async function resumeAuthorizedMcpTurn(args) {
16374
16227
  if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
16375
16228
  clearPendingAuth(conversation, pendingAuth.sessionId);
16376
16229
  await persistThreadStateById(threadId, { conversation });
16377
- await supersedeAgentTurnSessionCheckpoint({
16230
+ await abandonAgentTurnSessionRecord({
16378
16231
  conversationId: authSession.conversationId,
16379
16232
  sessionId: pendingAuth.sessionId,
16380
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16233
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16381
16234
  });
16382
16235
  return;
16383
16236
  }
@@ -16414,10 +16267,10 @@ async function resumeAuthorizedMcpTurn(args) {
16414
16267
  await persistThreadStateById(threadId, {
16415
16268
  conversation: lockedConversation
16416
16269
  });
16417
- await supersedeAgentTurnSessionCheckpoint({
16270
+ await abandonAgentTurnSessionRecord({
16418
16271
  conversationId: authSession.conversationId,
16419
16272
  sessionId: lockedPendingAuth.sessionId,
16420
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16273
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16421
16274
  });
16422
16275
  return false;
16423
16276
  }
@@ -16431,6 +16284,13 @@ async function resumeAuthorizedMcpTurn(args) {
16431
16284
  if (!lockedUserMessage) {
16432
16285
  return false;
16433
16286
  }
16287
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
16288
+ authSession.conversationId,
16289
+ lockedSessionId
16290
+ );
16291
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16292
+ return false;
16293
+ }
16434
16294
  const lockedConversationContext = buildConversationContext(
16435
16295
  lockedConversation,
16436
16296
  {
@@ -16440,6 +16300,17 @@ async function resumeAuthorizedMcpTurn(args) {
16440
16300
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
16441
16301
  authSession.channelId
16442
16302
  );
16303
+ await recordAuthorizationCompleted({
16304
+ conversationId: authSession.conversationId,
16305
+ kind: "mcp",
16306
+ provider,
16307
+ requesterId: authSession.userId,
16308
+ authorizationId: mcpAuthorizationId({
16309
+ provider,
16310
+ sessionId: lockedSessionId
16311
+ }),
16312
+ ttlMs: THREAD_STATE_TTL_MS4
16313
+ });
16443
16314
  return {
16444
16315
  messageText: lockedUserMessage.text,
16445
16316
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
@@ -16485,8 +16356,9 @@ async function resumeAuthorizedMcpTurn(args) {
16485
16356
  );
16486
16357
  },
16487
16358
  onPostDeliveryCommitFailure: async () => {
16488
- await failAgentTurnSessionCheckpoint({
16359
+ await failAgentTurnSessionRecord({
16489
16360
  conversationId: authSession.conversationId,
16361
+ expectedVersion: lockedSessionRecord.version,
16490
16362
  sessionId: lockedSessionId,
16491
16363
  errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
16492
16364
  });
@@ -16496,7 +16368,8 @@ async function resumeAuthorizedMcpTurn(args) {
16496
16368
  await persistFailedReplyState(
16497
16369
  authSession.channelId,
16498
16370
  authSession.threadTs,
16499
- lockedSessionId
16371
+ lockedSessionId,
16372
+ lockedSessionRecord.version
16500
16373
  );
16501
16374
  } catch (persistError) {
16502
16375
  logException(
@@ -16527,11 +16400,11 @@ async function resumeAuthorizedMcpTurn(args) {
16527
16400
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16528
16401
  throw error;
16529
16402
  }
16530
- const checkpointVersion = error.metadata?.checkpointVersion;
16403
+ const version = error.metadata?.version;
16531
16404
  const nextSliceId = error.metadata?.sliceId;
16532
- if (typeof checkpointVersion !== "number") {
16405
+ if (typeof version !== "number") {
16533
16406
  throw new Error(
16534
- "Timed-out MCP resume did not include a checkpoint version"
16407
+ "Timed-out MCP resume did not include a turn-session version"
16535
16408
  );
16536
16409
  }
16537
16410
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -16551,14 +16424,14 @@ async function resumeAuthorizedMcpTurn(args) {
16551
16424
  await scheduleTurnTimeoutResume({
16552
16425
  conversationId: authSession.conversationId,
16553
16426
  sessionId: lockedSessionId,
16554
- expectedCheckpointVersion: checkpointVersion
16427
+ expectedVersion: version
16555
16428
  });
16556
16429
  }
16557
16430
  };
16558
16431
  }
16559
16432
  });
16560
16433
  }
16561
- async function GET5(request, provider, waitUntil) {
16434
+ async function GET3(request, provider, waitUntil) {
16562
16435
  const url = new URL(request.url);
16563
16436
  const state = url.searchParams.get("state")?.trim();
16564
16437
  const code = url.searchParams.get("code")?.trim();
@@ -16604,9 +16477,12 @@ async function GET5(request, provider, waitUntil) {
16604
16477
  }
16605
16478
  }
16606
16479
 
16480
+ // src/handlers/oauth-callback.ts
16481
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS5 } from "chat";
16482
+
16607
16483
  // src/chat/slack/app-home.ts
16608
16484
  import fs5 from "fs";
16609
- import path10 from "path";
16485
+ import path9 from "path";
16610
16486
  var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
16611
16487
  var MAX_HOME_SKILLS = 6;
16612
16488
  var MAX_SECTION_TEXT_CHARS = 3e3;
@@ -16618,7 +16494,7 @@ function clampSectionText(text) {
16618
16494
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
16619
16495
  }
16620
16496
  function loadDescriptionText() {
16621
- const descriptionPath = path10.join(homeDir(), "DESCRIPTION.md");
16497
+ const descriptionPath = path9.join(homeDir(), "DESCRIPTION.md");
16622
16498
  try {
16623
16499
  const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
16624
16500
  if (raw.length > 0) {
@@ -16769,23 +16645,27 @@ async function persistCompletedOAuthReplyState(args) {
16769
16645
  ...statePatch
16770
16646
  });
16771
16647
  }
16772
- async function failCheckpointBestEffort2(args) {
16648
+ function pluginAuthorizationId(args) {
16649
+ return `${args.sessionId}:plugin:${args.provider}`;
16650
+ }
16651
+ async function failSessionRecordBestEffort2(args) {
16773
16652
  try {
16774
- await failAgentTurnSessionCheckpoint({
16653
+ await failAgentTurnSessionRecord({
16775
16654
  conversationId: args.conversationId,
16655
+ expectedVersion: args.expectedVersion,
16776
16656
  sessionId: args.sessionId,
16777
16657
  errorMessage: args.errorMessage
16778
16658
  });
16779
16659
  } catch (error) {
16780
16660
  logException(
16781
16661
  error,
16782
- "oauth_callback_checkpoint_fail_persist_failed",
16662
+ "oauth_callback_session_record_fail_persist_failed",
16783
16663
  {},
16784
16664
  {
16785
16665
  "app.ai.conversation_id": args.conversationId,
16786
16666
  "app.ai.session_id": args.sessionId
16787
16667
  },
16788
- "Failed to mark OAuth-resumed turn checkpoint failed"
16668
+ "Failed to mark OAuth-resumed turn session record failed"
16789
16669
  );
16790
16670
  }
16791
16671
  }
@@ -16801,8 +16681,9 @@ async function persistFailedOAuthReplyState(args) {
16801
16681
  markConversationMessage,
16802
16682
  updateConversationStats
16803
16683
  });
16804
- await failCheckpointBestEffort2({
16684
+ await failSessionRecordBestEffort2({
16805
16685
  conversationId: args.conversationId,
16686
+ expectedVersion: args.expectedVersion,
16806
16687
  sessionId: args.sessionId,
16807
16688
  errorMessage: "OAuth-resumed turn failed"
16808
16689
  });
@@ -16810,21 +16691,21 @@ async function persistFailedOAuthReplyState(args) {
16810
16691
  conversation
16811
16692
  });
16812
16693
  }
16813
- async function resumeCheckpointedOAuthTurn(stored) {
16694
+ async function resumeOAuthSessionRecordTurn(stored) {
16814
16695
  if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
16815
16696
  return false;
16816
16697
  }
16817
- const checkpoint = await getAgentTurnSessionCheckpoint(
16698
+ const sessionRecord = await getAgentTurnSessionRecord(
16818
16699
  stored.resumeConversationId,
16819
16700
  stored.resumeSessionId
16820
16701
  );
16821
- if (!checkpoint) {
16702
+ if (!sessionRecord) {
16822
16703
  return false;
16823
16704
  }
16824
- if (checkpoint.state === "completed" || checkpoint.state === "failed" || checkpoint.state === "superseded") {
16705
+ if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16825
16706
  return true;
16826
16707
  }
16827
- if (checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
16708
+ if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16828
16709
  return true;
16829
16710
  }
16830
16711
  const currentState = await getPersistedThreadState(
@@ -16845,10 +16726,10 @@ async function resumeCheckpointedOAuthTurn(stored) {
16845
16726
  await persistThreadStateById(stored.resumeConversationId, {
16846
16727
  conversation
16847
16728
  });
16848
- await supersedeAgentTurnSessionCheckpoint({
16729
+ await abandonAgentTurnSessionRecord({
16849
16730
  conversationId: stored.resumeConversationId,
16850
16731
  sessionId: pendingAuth.sessionId,
16851
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16732
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16852
16733
  });
16853
16734
  return true;
16854
16735
  }
@@ -16871,11 +16752,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
16871
16752
  lockKey: stored.resumeConversationId,
16872
16753
  initialText: "",
16873
16754
  beforeStart: async () => {
16874
- const lockedCheckpoint = await getAgentTurnSessionCheckpoint(
16755
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
16875
16756
  stored.resumeConversationId,
16876
16757
  stored.resumeSessionId
16877
16758
  );
16878
- if (!lockedCheckpoint || lockedCheckpoint.state !== "awaiting_resume" || lockedCheckpoint.resumeReason !== "auth") {
16759
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16879
16760
  return false;
16880
16761
  }
16881
16762
  const lockedState = await getPersistedThreadState(
@@ -16899,10 +16780,10 @@ async function resumeCheckpointedOAuthTurn(stored) {
16899
16780
  await persistThreadStateById(stored.resumeConversationId, {
16900
16781
  conversation: lockedConversation
16901
16782
  });
16902
- await supersedeAgentTurnSessionCheckpoint({
16783
+ await abandonAgentTurnSessionRecord({
16903
16784
  conversationId: stored.resumeConversationId,
16904
16785
  sessionId: lockedPendingAuth.sessionId,
16905
- errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16786
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
16906
16787
  });
16907
16788
  return false;
16908
16789
  }
@@ -16925,6 +16806,17 @@ async function resumeCheckpointedOAuthTurn(stored) {
16925
16806
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
16926
16807
  stored.channelId
16927
16808
  );
16809
+ await recordAuthorizationCompleted({
16810
+ conversationId: stored.resumeConversationId,
16811
+ kind: "plugin",
16812
+ provider: stored.provider,
16813
+ requesterId: stored.userId,
16814
+ authorizationId: pluginAuthorizationId({
16815
+ provider: stored.provider,
16816
+ sessionId: lockedSessionId
16817
+ }),
16818
+ ttlMs: THREAD_STATE_TTL_MS5
16819
+ });
16928
16820
  return {
16929
16821
  messageText: stored.pendingMessage ?? lockedUserMessage.text,
16930
16822
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
@@ -16969,7 +16861,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
16969
16861
  "app.ai.outcome": reply.diagnostics.outcome,
16970
16862
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
16971
16863
  },
16972
- "OAuth callback auto-resumed checkpoint finished replying"
16864
+ "OAuth callback auto-resumed session record finished replying"
16973
16865
  );
16974
16866
  await persistCompletedOAuthReplyState({
16975
16867
  conversationId: stored.resumeConversationId,
@@ -16978,9 +16870,9 @@ async function resumeCheckpointedOAuthTurn(stored) {
16978
16870
  });
16979
16871
  },
16980
16872
  onPostDeliveryCommitFailure: async () => {
16981
- await failAgentTurnSessionCheckpoint({
16873
+ await failAgentTurnSessionRecord({
16982
16874
  conversationId: stored.resumeConversationId,
16983
- expectedCheckpointVersion: lockedCheckpoint.checkpointVersion,
16875
+ expectedVersion: lockedSessionRecord.version,
16984
16876
  sessionId: lockedSessionId,
16985
16877
  errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
16986
16878
  });
@@ -16988,6 +16880,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
16988
16880
  onFailure: async () => {
16989
16881
  await persistFailedOAuthReplyState({
16990
16882
  conversationId: stored.resumeConversationId,
16883
+ expectedVersion: lockedSessionRecord.version,
16991
16884
  sessionId: lockedSessionId
16992
16885
  });
16993
16886
  },
@@ -17001,11 +16894,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
17001
16894
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
17002
16895
  throw error;
17003
16896
  }
17004
- const checkpointVersion = error.metadata?.checkpointVersion;
16897
+ const version = error.metadata?.version;
17005
16898
  const nextSliceId = error.metadata?.sliceId;
17006
- if (typeof checkpointVersion !== "number") {
16899
+ if (typeof version !== "number") {
17007
16900
  throw new Error(
17008
- "Timed-out OAuth resume did not include a checkpoint version"
16901
+ "Timed-out OAuth resume did not include a turn-session version"
17009
16902
  );
17010
16903
  }
17011
16904
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -17016,7 +16909,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
17016
16909
  await scheduleTurnTimeoutResume({
17017
16910
  conversationId: stored.resumeConversationId,
17018
16911
  sessionId: lockedSessionId,
17019
- expectedCheckpointVersion: checkpointVersion
16912
+ expectedVersion: version
17020
16913
  });
17021
16914
  }
17022
16915
  };
@@ -17060,7 +16953,7 @@ async function resumePendingOAuthMessage(stored) {
17060
16953
  }
17061
16954
  });
17062
16955
  }
17063
- async function GET6(request, provider, waitUntil) {
16956
+ async function GET4(request, provider, waitUntil) {
17064
16957
  const providerConfig = getPluginOAuthConfig(provider);
17065
16958
  if (!providerConfig) {
17066
16959
  return htmlErrorResponse(
@@ -17199,7 +17092,7 @@ async function GET6(request, provider, waitUntil) {
17199
17092
  if (stored.pendingMessage && stored.channelId && stored.threadTs) {
17200
17093
  waitUntil(async () => {
17201
17094
  try {
17202
- const resumed = await resumeCheckpointedOAuthTurn(stored);
17095
+ const resumed = await resumeOAuthSessionRecordTurn(stored);
17203
17096
  if (!resumed) {
17204
17097
  await resumePendingOAuthMessage(stored);
17205
17098
  }
@@ -17431,12 +17324,12 @@ function normalizePort(value) {
17431
17324
  function sandboxIdFromPayload(payload) {
17432
17325
  return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
17433
17326
  }
17434
- function normalizedForwardedPath(path11) {
17435
- if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
17327
+ function normalizedForwardedPath(path10) {
17328
+ if (!path10.startsWith("/") || path10.startsWith("//") || path10.includes("#") || /[\r\n]/.test(path10)) {
17436
17329
  return { ok: false, error: "Invalid forwarded path" };
17437
17330
  }
17438
17331
  try {
17439
- const url = new URL(path11, "https://sandbox-forwarded.local");
17332
+ const url = new URL(path10, "https://sandbox-forwarded.local");
17440
17333
  return { ok: true, path: `${url.pathname}${url.search}` };
17441
17334
  } catch {
17442
17335
  return { ok: false, error: "Invalid forwarded path" };
@@ -17471,13 +17364,13 @@ function buildUpstreamUrl(request) {
17471
17364
  if (forwardedPort && !port) {
17472
17365
  return { ok: false, error: "Invalid forwarded port" };
17473
17366
  }
17474
- const path11 = upstreamPath(request);
17475
- if (!path11.ok) {
17476
- return { ok: false, error: path11.error };
17367
+ const path10 = upstreamPath(request);
17368
+ if (!path10.ok) {
17369
+ return { ok: false, error: path10.error };
17477
17370
  }
17478
17371
  try {
17479
17372
  const url = new URL(
17480
- `${scheme}://${host}${port ? `:${port}` : ""}${path11.path}`
17373
+ `${scheme}://${host}${port ? `:${port}` : ""}${path10.path}`
17481
17374
  );
17482
17375
  return { ok: true, url };
17483
17376
  } catch {
@@ -17794,63 +17687,65 @@ function sleep4(ms) {
17794
17687
  }
17795
17688
  async function persistCompletedReplyState2(args) {
17796
17689
  const currentState = await getPersistedThreadState(
17797
- args.checkpoint.conversationId
17690
+ args.sessionRecord.conversationId
17798
17691
  );
17799
17692
  const conversation = coerceThreadConversationState(currentState);
17800
17693
  const artifacts = coerceThreadArtifactsState(currentState);
17801
17694
  const userMessage2 = getTurnUserMessage(
17802
17695
  conversation,
17803
- args.checkpoint.sessionId
17696
+ args.sessionRecord.sessionId
17804
17697
  );
17805
17698
  const statePatch = buildDeliveredTurnStatePatch({
17806
17699
  artifacts,
17807
17700
  conversation,
17808
17701
  reply: args.reply,
17809
- sessionId: args.checkpoint.sessionId,
17702
+ sessionId: args.sessionRecord.sessionId,
17810
17703
  userMessageId: userMessage2?.id
17811
17704
  });
17812
- await persistThreadStateById(args.checkpoint.conversationId, {
17705
+ await persistThreadStateById(args.sessionRecord.conversationId, {
17813
17706
  ...statePatch
17814
17707
  });
17815
17708
  }
17816
- async function failCheckpointBestEffort3(args) {
17709
+ async function failSessionRecordBestEffort3(args) {
17817
17710
  try {
17818
- await failAgentTurnSessionCheckpoint({
17819
- conversationId: args.checkpoint.conversationId,
17820
- expectedCheckpointVersion: args.checkpoint.checkpointVersion,
17821
- sessionId: args.checkpoint.sessionId,
17711
+ await failAgentTurnSessionRecord({
17712
+ conversationId: args.sessionRecord.conversationId,
17713
+ expectedVersion: args.sessionRecord.version,
17714
+ sessionId: args.sessionRecord.sessionId,
17822
17715
  errorMessage: args.errorMessage
17823
17716
  });
17824
17717
  } catch (error) {
17825
17718
  logException(
17826
17719
  error,
17827
- "timeout_resume_checkpoint_fail_persist_failed",
17720
+ "timeout_resume_session_record_fail_persist_failed",
17828
17721
  {},
17829
17722
  {
17830
- "app.ai.conversation_id": args.checkpoint.conversationId,
17831
- "app.ai.session_id": args.checkpoint.sessionId
17723
+ "app.ai.conversation_id": args.sessionRecord.conversationId,
17724
+ "app.ai.session_id": args.sessionRecord.sessionId
17832
17725
  },
17833
- "Failed to mark timed-out turn checkpoint failed"
17726
+ "Failed to mark timed-out turn session record failed"
17834
17727
  );
17835
17728
  }
17836
17729
  }
17837
- async function persistFailedReplyState2(checkpoint) {
17838
- const currentState = await getPersistedThreadState(checkpoint.conversationId);
17730
+ async function persistFailedReplyState2(sessionRecord) {
17731
+ const currentState = await getPersistedThreadState(
17732
+ sessionRecord.conversationId
17733
+ );
17839
17734
  const conversation = coerceThreadConversationState(currentState);
17840
- clearPendingAuth(conversation, checkpoint.sessionId);
17735
+ clearPendingAuth(conversation, sessionRecord.sessionId);
17841
17736
  markTurnFailed({
17842
17737
  conversation,
17843
17738
  nowMs: Date.now(),
17844
- sessionId: checkpoint.sessionId,
17845
- userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
17739
+ sessionId: sessionRecord.sessionId,
17740
+ userMessageId: getTurnUserMessage(conversation, sessionRecord.sessionId)?.id,
17846
17741
  markConversationMessage,
17847
17742
  updateConversationStats
17848
17743
  });
17849
- await failCheckpointBestEffort3({
17850
- checkpoint,
17744
+ await failSessionRecordBestEffort3({
17745
+ sessionRecord,
17851
17746
  errorMessage: "Timed-out turn failed while resuming"
17852
17747
  });
17853
- await persistThreadStateById(checkpoint.conversationId, {
17748
+ await persistThreadStateById(sessionRecord.conversationId, {
17854
17749
  conversation
17855
17750
  });
17856
17751
  }
@@ -17867,11 +17762,11 @@ async function resumeTimedOutTurn(payload) {
17867
17762
  threadTs: thread.threadTs,
17868
17763
  lockKey: payload.conversationId,
17869
17764
  beforeStart: async () => {
17870
- const checkpoint = await getAgentTurnSessionCheckpoint(
17765
+ const sessionRecord = await getAgentTurnSessionRecord(
17871
17766
  payload.conversationId,
17872
17767
  payload.sessionId
17873
17768
  );
17874
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
17769
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" || sessionRecord.version !== payload.expectedVersion) {
17875
17770
  return false;
17876
17771
  }
17877
17772
  const currentState = await getPersistedThreadState(
@@ -17931,16 +17826,16 @@ async function resumeTimedOutTurn(payload) {
17931
17826
  ...getTurnUserReplyAttachmentContext(userMessage2)
17932
17827
  },
17933
17828
  onSuccess: async (reply) => {
17934
- await persistCompletedReplyState2({ checkpoint, reply });
17829
+ await persistCompletedReplyState2({ sessionRecord, reply });
17935
17830
  },
17936
17831
  onFailure: async () => {
17937
- await persistFailedReplyState2(checkpoint);
17832
+ await persistFailedReplyState2(sessionRecord);
17938
17833
  },
17939
17834
  onPostDeliveryCommitFailure: async () => {
17940
- await failAgentTurnSessionCheckpoint({
17941
- conversationId: checkpoint.conversationId,
17942
- expectedCheckpointVersion: checkpoint.checkpointVersion,
17943
- sessionId: checkpoint.sessionId,
17835
+ await failAgentTurnSessionRecord({
17836
+ conversationId: sessionRecord.conversationId,
17837
+ expectedVersion: sessionRecord.version,
17838
+ sessionId: sessionRecord.sessionId,
17944
17839
  errorMessage: "Timed-out turn reply was delivered but completion state did not persist"
17945
17840
  });
17946
17841
  },
@@ -17963,11 +17858,11 @@ async function resumeTimedOutTurn(payload) {
17963
17858
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
17964
17859
  throw error;
17965
17860
  }
17966
- const checkpointVersion = error.metadata?.checkpointVersion;
17861
+ const version = error.metadata?.version;
17967
17862
  const nextSliceId = error.metadata?.sliceId;
17968
- if (typeof checkpointVersion !== "number") {
17863
+ if (typeof version !== "number") {
17969
17864
  throw new Error(
17970
- "Timed-out resume turn did not include a checkpoint version"
17865
+ "Timed-out resume turn did not include a turn-session version"
17971
17866
  );
17972
17867
  }
17973
17868
  if (!canScheduleTurnTimeoutResume(nextSliceId)) {
@@ -17988,7 +17883,7 @@ async function resumeTimedOutTurn(payload) {
17988
17883
  await scheduleTurnTimeoutResume({
17989
17884
  conversationId: payload.conversationId,
17990
17885
  sessionId: payload.sessionId,
17991
- expectedCheckpointVersion: checkpointVersion
17886
+ expectedVersion: version
17992
17887
  });
17993
17888
  }
17994
17889
  };
@@ -18383,6 +18278,26 @@ async function decideSubscribedThreadReply(args) {
18383
18278
  }
18384
18279
  }
18385
18280
 
18281
+ // src/chat/runtime/turn-input.ts
18282
+ function combineTextParts(queuedTexts, latestText) {
18283
+ const parts = [...queuedTexts, latestText].filter(
18284
+ (part) => part.trim().length > 0
18285
+ );
18286
+ return parts.length > 0 ? parts.join("\n\n") : latestText;
18287
+ }
18288
+ function combineTurnText(queuedMessages, latestText) {
18289
+ return {
18290
+ rawText: combineTextParts(
18291
+ queuedMessages.map((message) => message.rawText),
18292
+ latestText.rawText
18293
+ ),
18294
+ userText: combineTextParts(
18295
+ queuedMessages.map((message) => message.userText),
18296
+ latestText.userText
18297
+ )
18298
+ };
18299
+ }
18300
+
18386
18301
  // src/chat/runtime/slack-runtime.ts
18387
18302
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
18388
18303
  async function maybeHandleThreadOptOutDecision(args) {
@@ -18394,6 +18309,19 @@ async function maybeHandleThreadOptOutDecision(args) {
18394
18309
  await args.thread.post(THREAD_OPTOUT_ACK);
18395
18310
  return true;
18396
18311
  }
18312
+ function getQueuedMessages(context, options) {
18313
+ return (context?.skipped ?? []).map((message) => {
18314
+ const stripped = options.stripLeadingBotMention(message.text, {
18315
+ stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
18316
+ });
18317
+ return {
18318
+ explicitMention: options.explicitMention || Boolean(message.isMention),
18319
+ message,
18320
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
18321
+ userText: appendSlackLegacyAttachmentText(stripped, message.raw)
18322
+ };
18323
+ });
18324
+ }
18397
18325
  function buildLogContext(deps, args) {
18398
18326
  return {
18399
18327
  conversationId: args.threadId ?? args.runId,
@@ -18463,7 +18391,7 @@ function createSlackTurnRuntime(deps) {
18463
18391
  message: args.message,
18464
18392
  decision: args.decision,
18465
18393
  completedAtMs,
18466
- userText: args.userText
18394
+ text: args.text
18467
18395
  });
18468
18396
  }
18469
18397
  };
@@ -18493,9 +18421,14 @@ function createSlackTurnRuntime(deps) {
18493
18421
  );
18494
18422
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
18495
18423
  await thread.subscribe();
18424
+ const queuedMessages = getQueuedMessages(hooks?.messageContext, {
18425
+ explicitMention: true,
18426
+ stripLeadingBotMention: deps.stripLeadingBotMention
18427
+ });
18496
18428
  await deps.replyToThread(thread, message, {
18497
18429
  explicitMention: true,
18498
18430
  beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
18431
+ queuedMessages,
18499
18432
  onToolInvocation: toolInvocationHook
18500
18433
  });
18501
18434
  });
@@ -18558,28 +18491,33 @@ function createSlackTurnRuntime(deps) {
18558
18491
  const legacyAttachmentText = renderSlackLegacyAttachmentText(
18559
18492
  message.raw
18560
18493
  );
18561
- const rawUserText = appendSlackLegacyAttachmentText(
18562
- message.text,
18563
- message.raw
18564
- );
18565
18494
  const strippedUserText = deps.stripLeadingBotMention(message.text, {
18566
18495
  stripLeadingSlackMentionToken: Boolean(message.isMention)
18567
18496
  });
18568
- const userText = appendSlackLegacyAttachmentText(
18569
- strippedUserText,
18570
- message.raw
18571
- );
18497
+ const currentText = {
18498
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
18499
+ userText: appendSlackLegacyAttachmentText(
18500
+ strippedUserText,
18501
+ message.raw
18502
+ )
18503
+ };
18572
18504
  const threadContext = {
18573
18505
  threadId,
18574
18506
  requesterId: message.author.userId,
18575
18507
  channelId,
18576
18508
  runId
18577
18509
  };
18510
+ const queuedMessages = getQueuedMessages(hooks?.messageContext, {
18511
+ explicitMention: Boolean(message.isMention),
18512
+ stripLeadingBotMention: deps.stripLeadingBotMention
18513
+ });
18514
+ const combinedText = combineTurnText(queuedMessages, currentText);
18515
+ const turnIsExplicitMention = Boolean(message.isMention) || queuedMessages.some((queued) => queued.explicitMention);
18578
18516
  const preflightDecision = getSubscribedReplyPreflightDecision({
18579
18517
  botUserName: deps.assistantUserName,
18580
- rawText: rawUserText,
18581
- text: userText,
18582
- isExplicitMention: Boolean(message.isMention)
18518
+ rawText: combinedText.rawText,
18519
+ text: combinedText.userText,
18520
+ isExplicitMention: turnIsExplicitMention
18583
18521
  });
18584
18522
  if (preflightDecision && !preflightDecision.shouldReply) {
18585
18523
  const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
@@ -18588,27 +18526,30 @@ function createSlackTurnRuntime(deps) {
18588
18526
  message,
18589
18527
  decision: { shouldReply: false, reason },
18590
18528
  context: threadContext,
18591
- userText
18529
+ text: combinedText
18592
18530
  });
18593
18531
  return;
18594
18532
  }
18595
18533
  const preparedState = await deps.prepareTurnState({
18596
18534
  thread,
18597
18535
  message,
18598
- userText,
18536
+ text: currentText,
18599
18537
  explicitMention: Boolean(message.isMention),
18600
- context: threadContext
18538
+ context: threadContext,
18539
+ queuedMessages
18601
18540
  });
18602
18541
  await deps.persistPreparedState({
18603
18542
  thread,
18604
18543
  preparedState
18605
18544
  });
18606
18545
  const decision = await deps.decideSubscribedReply({
18607
- rawText: rawUserText,
18608
- text: userText,
18546
+ rawText: combinedText.rawText,
18547
+ text: combinedText.userText,
18609
18548
  conversationContext: deps.getPreparedConversationContext(preparedState),
18610
- hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
18611
- isExplicitMention: Boolean(message.isMention),
18549
+ hasAttachments: message.attachments.length > 0 || queuedMessages.some(
18550
+ (queued) => queued.message.attachments.length > 0
18551
+ ) || legacyAttachmentText !== "",
18552
+ isExplicitMention: turnIsExplicitMention,
18612
18553
  context: threadContext
18613
18554
  });
18614
18555
  if (await maybeHandleThreadOptOutDecision({
@@ -18622,7 +18563,7 @@ function createSlackTurnRuntime(deps) {
18622
18563
  decision,
18623
18564
  context: threadContext,
18624
18565
  preparedState,
18625
- userText
18566
+ text: combinedText
18626
18567
  });
18627
18568
  return;
18628
18569
  }
@@ -18633,7 +18574,7 @@ function createSlackTurnRuntime(deps) {
18633
18574
  decision,
18634
18575
  context: threadContext,
18635
18576
  preparedState,
18636
- userText
18577
+ text: combinedText
18637
18578
  });
18638
18579
  return;
18639
18580
  }
@@ -18651,6 +18592,7 @@ function createSlackTurnRuntime(deps) {
18651
18592
  explicitMention: Boolean(message.isMention),
18652
18593
  preparedState,
18653
18594
  beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
18595
+ queuedMessages,
18654
18596
  onToolInvocation: toolInvocationHook
18655
18597
  });
18656
18598
  });
@@ -18748,6 +18690,7 @@ function createSlackTurnRuntime(deps) {
18748
18690
  }
18749
18691
 
18750
18692
  // src/chat/services/context-compaction.ts
18693
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS6 } from "chat";
18751
18694
  import {
18752
18695
  estimateContextTokens,
18753
18696
  estimateTokens
@@ -18930,22 +18873,8 @@ function buildReplacementHistory(args) {
18930
18873
  ${args.summary}`)
18931
18874
  ];
18932
18875
  }
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) {
18876
+ function loadCompactionSource(messages) {
18877
+ if (messages.length > 0) {
18949
18878
  return {
18950
18879
  estimatedTokens: estimateHistoryTokens(messages),
18951
18880
  messages
@@ -18954,10 +18883,7 @@ async function loadCompactionSource(args) {
18954
18883
  return { reason: "missing_context" };
18955
18884
  }
18956
18885
  async function maybeCompactWithDeps(args, deps) {
18957
- const source = await loadCompactionSource({
18958
- conversationId: args.conversationId,
18959
- previousSessionId: args.previousSessionId
18960
- });
18886
+ const source = loadCompactionSource(args.piMessages);
18961
18887
  if ("reason" in source) {
18962
18888
  return { compacted: false, reason: source.reason };
18963
18889
  }
@@ -19004,29 +18930,22 @@ async function writeCompactedThreadContext(args, sourceMessages, summary, contex
19004
18930
  messages: trimTrailingAssistantMessages(sourceMessages),
19005
18931
  summary
19006
18932
  });
19007
- const nextSessionId = createCompactionSessionId(args.previousSessionId);
19008
- await upsertAgentTurnSessionCheckpoint({
18933
+ await commitMessages({
19009
18934
  conversationId: args.conversationId,
19010
- sessionId: nextSessionId,
19011
- sliceId: 1,
19012
- state: "completed",
19013
- piMessages: replacement
18935
+ messages: replacement,
18936
+ ttlMs: THREAD_STATE_TTL_MS6
19014
18937
  });
19015
- args.conversation.processing.lastSessionId = nextSessionId;
19016
18938
  updateConversationStats(args.conversation);
19017
18939
  setSpanAttributes({
19018
18940
  "app.compaction.input_messages": sourceMessages.length,
19019
18941
  "app.compaction.retained_messages": replacement.length - 1,
19020
18942
  "app.compaction.summary_chars": summary.length,
19021
- "app.compaction.previous_session_id": args.previousSessionId,
19022
- "app.compaction.next_session_id": nextSessionId,
19023
18943
  ...context.triggerTokens !== void 0 ? { "app.compaction.trigger_tokens": context.triggerTokens } : {},
19024
18944
  "app.context_tokens_estimated": context.estimatedTokens
19025
18945
  });
19026
18946
  return {
19027
18947
  compacted: true,
19028
- piMessages: replacement,
19029
- sessionId: nextSessionId
18948
+ piMessages: replacement
19030
18949
  };
19031
18950
  }
19032
18951
  function createContextCompactor(deps) {
@@ -19736,7 +19655,7 @@ function maybeUpdateAssistantTitle(args) {
19736
19655
  assistantThreadContext.threadTs,
19737
19656
  title
19738
19657
  );
19739
- return titleSourceMessage.id;
19658
+ return { sourceMessageId: titleSourceMessage.id, title };
19740
19659
  } catch (error) {
19741
19660
  const slackErrorCode = getSlackApiErrorCode(error);
19742
19661
  const assistantTitleErrorAttributes = {
@@ -19760,7 +19679,7 @@ function maybeUpdateAssistantTitle(args) {
19760
19679
  assistantTitleErrorAttributes,
19761
19680
  "Skipping thread title update due to Slack permission error"
19762
19681
  );
19763
- return titleSourceMessage.id;
19682
+ return { sourceMessageId: titleSourceMessage.id };
19764
19683
  }
19765
19684
  logWarn(
19766
19685
  "thread_title_generation_failed",
@@ -19814,6 +19733,27 @@ function collectCanvasUrls(artifacts) {
19814
19733
  ].filter((url) => typeof url === "string" && url !== "")
19815
19734
  );
19816
19735
  }
19736
+ function turnRequester(args) {
19737
+ const requester = {
19738
+ ...args.email ? { email: args.email } : {},
19739
+ ...args.fullName ? { fullName: args.fullName } : {},
19740
+ ...args.userId ? { slackUserId: args.userId } : {},
19741
+ ...args.userName ? { slackUserName: args.userName } : {}
19742
+ };
19743
+ return Object.keys(requester).length > 0 ? requester : void 0;
19744
+ }
19745
+ async function resolveChannelName(thread) {
19746
+ const existingName = thread.channel.name?.trim();
19747
+ if (existingName) {
19748
+ return existingName;
19749
+ }
19750
+ try {
19751
+ const metadata = await thread.channel.fetchMetadata();
19752
+ return metadata.name?.trim() || void 0;
19753
+ } catch {
19754
+ return void 0;
19755
+ }
19756
+ }
19817
19757
  function getCurrentTurnCanvasUrl(args) {
19818
19758
  const previousUrls = collectCanvasUrls(args.before);
19819
19759
  const latestUrls = collectCanvasUrls(args.after);
@@ -19827,35 +19767,37 @@ function getCurrentTurnCanvasUrl(args) {
19827
19767
  function buildCanvasRecoveryReply(canvasUrl) {
19828
19768
  return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
19829
19769
  }
19770
+ function collectTurnAttachments(message, queuedMessages) {
19771
+ return [
19772
+ ...(queuedMessages ?? []).flatMap((queued) => queued.message.attachments),
19773
+ ...message.attachments
19774
+ ];
19775
+ }
19830
19776
  async function loadPiMessagesForTurn(args) {
19831
19777
  const fallback = args.fallback.length > 0 ? [...args.fallback] : void 0;
19832
19778
  if (!args.conversationId) {
19833
19779
  return { piMessages: fallback };
19834
19780
  }
19835
19781
  if (args.activeTurnId) {
19836
- const checkpoint2 = await getAgentTurnSessionCheckpoint(
19782
+ const sessionRecord = await getAgentTurnSessionRecord(
19837
19783
  args.conversationId,
19838
19784
  args.activeTurnId
19839
19785
  );
19840
- if (checkpoint2?.piMessages.length) {
19786
+ if (sessionRecord?.piMessages.length) {
19841
19787
  return {
19842
19788
  piMessages: stripRuntimeTurnContext(
19843
- trimTrailingAssistantMessages(checkpoint2.piMessages)
19789
+ trimTrailingAssistantMessages(sessionRecord.piMessages)
19844
19790
  )
19845
19791
  };
19846
19792
  }
19847
19793
  }
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) {
19794
+ const projection = await loadProjection({
19795
+ conversationId: args.conversationId
19796
+ });
19797
+ if (projection.length > 0) {
19856
19798
  return {
19857
- compactionSessionId: args.lastSessionId,
19858
- piMessages: stripRuntimeTurnContext(checkpoint.piMessages)
19799
+ canCompact: true,
19800
+ piMessages: projection
19859
19801
  };
19860
19802
  }
19861
19803
  return { piMessages: fallback };
@@ -19867,6 +19809,7 @@ function createReplyToThread(deps) {
19867
19809
  }
19868
19810
  const threadId = getThreadId(thread, message);
19869
19811
  const channelId = getChannelId(thread, message);
19812
+ const channelName = channelId ? await resolveChannelName(thread) : void 0;
19870
19813
  const threadTs = getThreadTs(threadId);
19871
19814
  const assistantThreadContext = getAssistantThreadContext(message);
19872
19815
  const messageTs = getMessageTs(message);
@@ -19889,17 +19832,25 @@ function createReplyToThread(deps) {
19889
19832
  const strippedUserText = stripLeadingBotMention(message.text, {
19890
19833
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
19891
19834
  });
19892
- const userText = appendSlackLegacyAttachmentText(
19893
- strippedUserText,
19894
- message.raw
19895
- );
19835
+ const currentText = {
19836
+ rawText: appendSlackLegacyAttachmentText(message.text, message.raw),
19837
+ userText: appendSlackLegacyAttachmentText(
19838
+ strippedUserText,
19839
+ message.raw
19840
+ )
19841
+ };
19842
+ const effectiveUserText = combineTurnText(
19843
+ options.queuedMessages ?? [],
19844
+ currentText
19845
+ ).userText;
19896
19846
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
19897
19847
  thread,
19898
19848
  message,
19899
- userText,
19849
+ text: currentText,
19900
19850
  explicitMention: Boolean(
19901
19851
  options.explicitMention || message.isMention
19902
19852
  ),
19853
+ queuedMessages: options.queuedMessages,
19903
19854
  context: {
19904
19855
  threadId,
19905
19856
  requesterId: message.author.userId,
@@ -19909,6 +19860,15 @@ function createReplyToThread(deps) {
19909
19860
  });
19910
19861
  const slackMessageTs = getSlackMessageTs(message);
19911
19862
  const turnId = buildDeterministicTurnId(message.id);
19863
+ const fallbackIdentity = await deps.services.lookupSlackUser(
19864
+ message.author.userId
19865
+ );
19866
+ const requester = turnRequester({
19867
+ email: fallbackIdentity?.email,
19868
+ fullName: message.author.fullName ?? fallbackIdentity?.fullName,
19869
+ userId: message.author.userId,
19870
+ userName: message.author.userName ?? fallbackIdentity?.userName
19871
+ });
19912
19872
  const turnTraceContext = {
19913
19873
  conversationId,
19914
19874
  slackThreadId: threadId,
@@ -19990,7 +19950,7 @@ function createReplyToThread(deps) {
19990
19950
  "agent_turn_continuation_retry_schedule_failed",
19991
19951
  turnTraceContext,
19992
19952
  {
19993
- "app.ai.resume_checkpoint_version": resumeRequest.expectedCheckpointVersion,
19953
+ "app.ai.resume_session_version": resumeRequest.expectedVersion,
19994
19954
  "app.ai.resume_session_id": resumeRequest.sessionId,
19995
19955
  ...messageTs ? { "messaging.message.id": messageTs } : {}
19996
19956
  },
@@ -20013,11 +19973,10 @@ function createReplyToThread(deps) {
20013
19973
  return;
20014
19974
  }
20015
19975
  }
20016
- const lastSessionIdForHistory = preparedState.conversation.processing.lastSessionId;
20017
19976
  const configReply = await maybeApplyProviderDefaultConfigRequest({
20018
19977
  channelConfiguration: preparedState.channelConfiguration,
20019
19978
  requesterId: message.author.userId,
20020
- text: userText
19979
+ text: effectiveUserText
20021
19980
  });
20022
19981
  if (configReply) {
20023
19982
  await beforeFirstResponsePost();
@@ -20053,6 +20012,28 @@ function createReplyToThread(deps) {
20053
20012
  nextTurnId: turnId,
20054
20013
  updateConversationStats
20055
20014
  });
20015
+ if (conversationId) {
20016
+ void recordAgentTurnSessionSummary({
20017
+ channelName,
20018
+ conversationId,
20019
+ sessionId: turnId,
20020
+ sliceId: 1,
20021
+ startedAtMs: message.metadata.dateSent.getTime(),
20022
+ state: "running",
20023
+ requester,
20024
+ traceId: getActiveTraceId()
20025
+ }).catch((error) => {
20026
+ logException(
20027
+ error,
20028
+ "agent_turn_summary_record_failed",
20029
+ turnTraceContext,
20030
+ {
20031
+ "app.agent.turn.state": "running"
20032
+ },
20033
+ "Failed to record running turn summary"
20034
+ );
20035
+ });
20036
+ }
20056
20037
  setTags({
20057
20038
  conversationId
20058
20039
  });
@@ -20070,15 +20051,16 @@ function createReplyToThread(deps) {
20070
20051
  await persistThreadState(thread, {
20071
20052
  conversation: preparedState.conversation
20072
20053
  });
20073
- const fallbackIdentity = await deps.services.lookupSlackUser(
20074
- message.author.userId
20075
- );
20076
20054
  const resolvedUserName = message.author.userName ?? fallbackIdentity?.userName;
20077
20055
  if (resolvedUserName) {
20078
20056
  setTags({ slackUserName: resolvedUserName });
20079
20057
  }
20058
+ const turnAttachments = collectTurnAttachments(
20059
+ message,
20060
+ options.queuedMessages
20061
+ );
20080
20062
  const userAttachments = await deps.resolveUserAttachments(
20081
- message.attachments,
20063
+ turnAttachments,
20082
20064
  {
20083
20065
  threadId,
20084
20066
  requesterId: message.author.userId,
@@ -20088,7 +20070,7 @@ function createReplyToThread(deps) {
20088
20070
  messageTs: slackMessageTs
20089
20071
  }
20090
20072
  );
20091
- const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
20073
+ const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(turnAttachments) ? countPotentialImageAttachments(turnAttachments) : 0;
20092
20074
  const status = createSlackAdapterAssistantStatusSession({
20093
20075
  channelId: assistantThreadContext?.channelId,
20094
20076
  threadTs: assistantThreadContext?.threadTs,
@@ -20123,11 +20105,10 @@ function createReplyToThread(deps) {
20123
20105
  const loadedPiMessages = await loadPiMessagesForTurn({
20124
20106
  conversationId,
20125
20107
  activeTurnId,
20126
- lastSessionId: lastSessionIdForHistory,
20127
20108
  fallback: preparedState.conversation.piMessages
20128
20109
  });
20129
20110
  let piMessages = loadedPiMessages.piMessages;
20130
- if (conversationId && loadedPiMessages.compactionSessionId && piMessages?.length) {
20111
+ if (conversationId && loadedPiMessages.canCompact && piMessages?.length) {
20131
20112
  const compaction = await deps.services.contextCompactor.maybeCompact({
20132
20113
  conversation: preparedState.conversation,
20133
20114
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
@@ -20139,7 +20120,7 @@ function createReplyToThread(deps) {
20139
20120
  runId
20140
20121
  },
20141
20122
  onCompactionStart: () => status.start(compactingStatus),
20142
- previousSessionId: loadedPiMessages.compactionSessionId
20123
+ piMessages
20143
20124
  });
20144
20125
  if (compaction.compacted) {
20145
20126
  piMessages = compaction.piMessages;
@@ -20163,61 +20144,65 @@ function createReplyToThread(deps) {
20163
20144
  threadId
20164
20145
  });
20165
20146
  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,
20147
+ let reply = await deps.services.generateAssistantReply(
20148
+ effectiveUserText,
20149
+ {
20150
+ requester: {
20151
+ userId: message.author.userId,
20152
+ userName: message.author.userName ?? fallbackIdentity?.userName,
20153
+ fullName: message.author.fullName ?? fallbackIdentity?.fullName,
20154
+ email: fallbackIdentity?.email
20155
+ },
20156
+ conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
20157
+ artifactState: preparedState.artifacts,
20158
+ piMessages,
20159
+ pendingAuth: preparedState.conversation.processing.pendingAuth,
20160
+ configuration: preparedState.configuration,
20161
+ channelConfiguration: preparedState.channelConfiguration,
20162
+ inboundAttachmentCount: turnAttachments.length,
20163
+ omittedImageAttachmentCount,
20164
+ userAttachments,
20165
+ correlation: {
20211
20166
  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
- });
20167
+ threadId,
20168
+ turnId,
20169
+ threadTs,
20170
+ messageTs,
20171
+ teamId,
20172
+ runId,
20173
+ channelId,
20174
+ channelName,
20175
+ requesterId: message.author.userId
20176
+ },
20177
+ toolChannelId,
20178
+ sandbox: {
20179
+ sandboxId: preparedState.sandboxId,
20180
+ sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
20181
+ },
20182
+ onSandboxAcquired: async (sandbox) => {
20183
+ await persistThreadState(thread, {
20184
+ sandboxId: sandbox.sandboxId,
20185
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
20186
+ });
20187
+ },
20188
+ onArtifactStateUpdated: async (artifacts) => {
20189
+ latestArtifacts = artifacts;
20190
+ await persistThreadState(thread, { artifacts });
20191
+ },
20192
+ onAuthPending: async (pendingAuth) => {
20193
+ await applyPendingAuthUpdate({
20194
+ conversation: preparedState.conversation,
20195
+ conversationId,
20196
+ nextPendingAuth: pendingAuth
20197
+ });
20198
+ await persistThreadState(thread, {
20199
+ conversation: preparedState.conversation
20200
+ });
20201
+ },
20202
+ onStatus: (nextStatus) => status.update(nextStatus),
20203
+ onToolInvocation: options.onToolInvocation
20204
+ }
20205
+ );
20221
20206
  const diagnosticsContext = {
20222
20207
  slackThreadId: threadId,
20223
20208
  slackUserId: message.author.userId,
@@ -20305,7 +20290,10 @@ function createReplyToThread(deps) {
20305
20290
  }
20306
20291
  const titleUpdateResult = await assistantTitleTask;
20307
20292
  if (titleUpdateResult) {
20308
- artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
20293
+ artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult.sourceMessageId;
20294
+ if (titleUpdateResult.title) {
20295
+ artifactStatePatch.assistantTitle = titleUpdateResult.title;
20296
+ }
20309
20297
  }
20310
20298
  const completedState = buildDeliveredTurnStatePatch({
20311
20299
  artifactStatePatch,
@@ -20318,6 +20306,21 @@ function createReplyToThread(deps) {
20318
20306
  await persistThreadState(thread, {
20319
20307
  ...completedState
20320
20308
  });
20309
+ if (conversationId) {
20310
+ await recordAgentTurnSessionSummary({
20311
+ channelName,
20312
+ conversationId,
20313
+ cumulativeDurationMs: reply.diagnostics.durationMs,
20314
+ cumulativeUsage: reply.diagnostics.usage,
20315
+ sessionId: turnId,
20316
+ sliceId: 1,
20317
+ startedAtMs: message.metadata.dateSent.getTime(),
20318
+ state: "completed",
20319
+ conversationTitle: titleUpdateResult?.title,
20320
+ requester,
20321
+ traceId: getActiveTraceId()
20322
+ });
20323
+ }
20321
20324
  preparedState.conversation = completedState.conversation;
20322
20325
  persistedAtLeastOnce = true;
20323
20326
  if (shouldEmitDevAgentTrace()) {
@@ -20349,14 +20352,14 @@ function createReplyToThread(deps) {
20349
20352
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
20350
20353
  const conversationIdForResume = error.metadata?.conversationId;
20351
20354
  const sessionIdForResume = error.metadata?.sessionId;
20352
- const checkpointVersion = error.metadata?.checkpointVersion;
20355
+ const version = error.metadata?.version;
20353
20356
  const nextSliceId = error.metadata?.sliceId;
20354
- if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
20357
+ if (conversationIdForResume && sessionIdForResume && typeof version === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
20355
20358
  try {
20356
20359
  await deps.services.scheduleTurnTimeoutResume({
20357
20360
  conversationId: conversationIdForResume,
20358
20361
  sessionId: sessionIdForResume,
20359
- expectedCheckpointVersion: checkpointVersion
20362
+ expectedVersion: version
20360
20363
  });
20361
20364
  shouldPersistFailureState = false;
20362
20365
  } catch (scheduleError) {
@@ -20366,7 +20369,7 @@ function createReplyToThread(deps) {
20366
20369
  turnTraceContext,
20367
20370
  {
20368
20371
  ...messageTs ? { "messaging.message.id": messageTs } : {},
20369
- "app.ai.resume_checkpoint_version": checkpointVersion
20372
+ "app.ai.resume_session_version": version
20370
20373
  },
20371
20374
  "Failed to schedule timeout resume callback"
20372
20375
  );
@@ -20375,7 +20378,7 @@ function createReplyToThread(deps) {
20375
20378
  }
20376
20379
  await postTurnContinuationNotice();
20377
20380
  return;
20378
- } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
20381
+ } else if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
20379
20382
  logWarn(
20380
20383
  "agent_turn_timeout_resume_slice_limit_reached",
20381
20384
  turnTraceContext,
@@ -20465,18 +20468,35 @@ function createReplyToThread(deps) {
20465
20468
  });
20466
20469
  if (conversationId) {
20467
20470
  try {
20468
- await failAgentTurnSessionCheckpoint({
20471
+ await recordAgentTurnSessionSummary({
20472
+ channelName,
20469
20473
  conversationId,
20470
20474
  sessionId: turnId,
20471
- errorMessage: "Agent turn failed before final reply delivery completed"
20475
+ sliceId: 1,
20476
+ startedAtMs: message.metadata.dateSent.getTime(),
20477
+ state: "failed",
20478
+ requester,
20479
+ traceId: getActiveTraceId()
20472
20480
  });
20473
- } catch (checkpointError) {
20481
+ const sessionRecord = await getAgentTurnSessionRecord(
20482
+ conversationId,
20483
+ turnId
20484
+ );
20485
+ if (sessionRecord) {
20486
+ await failAgentTurnSessionRecord({
20487
+ conversationId,
20488
+ expectedVersion: sessionRecord.version,
20489
+ sessionId: turnId,
20490
+ errorMessage: "Agent turn failed before final reply delivery completed"
20491
+ });
20492
+ }
20493
+ } catch (recordError) {
20474
20494
  logException(
20475
- checkpointError,
20476
- "agent_turn_failed_checkpoint_persist_failed",
20495
+ recordError,
20496
+ "agent_turn_failed_session_record_persist_failed",
20477
20497
  turnTraceContext,
20478
20498
  {},
20479
- "Failed to mark failed turn checkpoint"
20499
+ "Failed to mark failed turn session record"
20480
20500
  );
20481
20501
  }
20482
20502
  }
@@ -20533,35 +20553,51 @@ async function refreshAssistantThreadContext(event) {
20533
20553
  await syncAssistantThreadContext(event, { setInitialTitle: false });
20534
20554
  }
20535
20555
 
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
20556
+ // src/chat/runtime/conversation-message.ts
20557
+ var NON_TEXT_MESSAGE_TEXT = "[non-text message]";
20558
+ function resolveMessageText(args) {
20559
+ const text = normalizeConversationText(args.text);
20560
+ return text || NON_TEXT_MESSAGE_TEXT;
20561
+ }
20562
+ function toConversationMessage(args) {
20563
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
20564
+ args.entry.attachments
20541
20565
  );
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
- }
20566
+ const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.entry.attachments) : 0;
20549
20567
  return {
20550
- id: entry.id,
20551
- role: entry.author.isMe ? "assistant" : "user",
20552
- text: rawText,
20553
- createdAtMs: entry.metadata.dateSent.getTime(),
20568
+ id: args.entry.id,
20569
+ role: args.entry.author.isMe ? "assistant" : "user",
20570
+ text: resolveMessageText(args),
20571
+ createdAtMs: args.entry.metadata.dateSent.getTime(),
20554
20572
  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
20573
+ userId: args.entry.author.userId,
20574
+ userName: args.entry.author.userName,
20575
+ fullName: args.entry.author.fullName,
20576
+ isBot: typeof args.entry.author.isBot === "boolean" ? args.entry.author.isBot : void 0
20559
20577
  },
20560
20578
  meta: {
20561
- slackTs: getSlackMessageTs(entry)
20579
+ attachmentCount: args.entry.attachments.length,
20580
+ explicitMention: args.explicitMention,
20581
+ imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
20582
+ imagesHydrated: !messageHasPotentialImageAttachment,
20583
+ slackTs: getSlackMessageTs(args.entry)
20562
20584
  }
20563
20585
  };
20564
20586
  }
20587
+
20588
+ // src/chat/runtime/turn-preparation.ts
20589
+ var BACKFILL_MESSAGE_LIMIT = 80;
20590
+ function hasPendingImageHydration(conversation) {
20591
+ return conversation.messages.some(
20592
+ (message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
20593
+ );
20594
+ }
20595
+ function getBackfillText(entry) {
20596
+ const text = normalizeConversationText(
20597
+ appendSlackLegacyAttachmentText(entry.text, entry.raw)
20598
+ );
20599
+ return text || void 0;
20600
+ }
20565
20601
  async function seedConversationBackfill(thread, conversation, currentTurn) {
20566
20602
  if (conversation.backfill.completedAtMs) {
20567
20603
  return;
@@ -20586,9 +20622,9 @@ async function seedConversationBackfill(thread, conversation, currentTurn) {
20586
20622
  }
20587
20623
  fetchedNewestFirst.reverse();
20588
20624
  for (const entry of fetchedNewestFirst) {
20589
- const message = createConversationMessageFromSdkMessage(entry);
20590
- if (message) {
20591
- seeded.push(message);
20625
+ const text = getBackfillText(entry);
20626
+ if (text) {
20627
+ seeded.push(toConversationMessage({ entry, text }));
20592
20628
  }
20593
20629
  }
20594
20630
  if (seeded.length > 0) {
@@ -20603,9 +20639,9 @@ async function seedConversationBackfill(thread, conversation, currentTurn) {
20603
20639
  }
20604
20640
  const fromRecent = thread.recentMessages.slice(-BACKFILL_MESSAGE_LIMIT);
20605
20641
  for (const entry of fromRecent) {
20606
- const message = createConversationMessageFromSdkMessage(entry);
20607
- if (message) {
20608
- seeded.push(message);
20642
+ const text = getBackfillText(entry);
20643
+ if (text) {
20644
+ seeded.push(toConversationMessage({ entry, text }));
20609
20645
  }
20610
20646
  }
20611
20647
  source = "recent_messages";
@@ -20642,35 +20678,26 @@ function createPrepareTurnState(deps) {
20642
20678
  messageId: args.message.id,
20643
20679
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
20644
20680
  });
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
- };
20681
+ for (const queued of args.queuedMessages ?? []) {
20682
+ const queuedMessage = toConversationMessage({
20683
+ entry: queued.message,
20684
+ explicitMention: queued.explicitMention,
20685
+ text: queued.userText
20686
+ });
20687
+ upsertConversationMessage(conversation, queuedMessage);
20688
+ }
20689
+ const incomingUserMessage = toConversationMessage({
20690
+ entry: args.message,
20691
+ explicitMention: args.explicitMention,
20692
+ text: args.text.userText
20693
+ });
20670
20694
  const userMessageId = upsertConversationMessage(
20671
20695
  conversation,
20672
20696
  incomingUserMessage
20673
20697
  );
20698
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(args.message.attachments) || (args.queuedMessages ?? []).some(
20699
+ (queued) => hasPotentialImageAttachment(queued.message.attachments)
20700
+ );
20674
20701
  const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
20675
20702
  if (isVisionEnabled() && shouldHydrateVisionContext) {
20676
20703
  await deps.hydrateConversationVisionContext(conversation, {
@@ -20757,28 +20784,20 @@ function createSlackRuntime(options) {
20757
20784
  message,
20758
20785
  decision,
20759
20786
  completedAtMs,
20760
- userText
20787
+ text
20761
20788
  }) => {
20762
20789
  const conversation = coerceThreadConversationState(await thread.state);
20763
- const normalizedUserText = normalizeConversationText(userText) || "[non-text message]";
20764
- const slackTs = getSlackMessageTs(message);
20790
+ const conversationMessage = toConversationMessage({
20791
+ entry: message,
20792
+ explicitMention: Boolean(message.isMention),
20793
+ text: text.userText
20794
+ });
20765
20795
  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
- },
20796
+ ...conversationMessage,
20776
20797
  meta: {
20777
- explicitMention: Boolean(message.isMention),
20778
- slackTs,
20798
+ ...conversationMessage.meta,
20779
20799
  replied: false,
20780
- skippedReason: decision.reason,
20781
- imagesHydrated: !hasPotentialImageAttachment(message.attachments)
20800
+ skippedReason: decision.reason
20782
20801
  }
20783
20802
  });
20784
20803
  conversation.processing.activeTurnId = void 0;
@@ -21221,17 +21240,26 @@ function createProductionBot() {
21221
21240
  });
21222
21241
  }
21223
21242
  function registerProductionHandlers(bot, slackRuntime) {
21224
- bot.onNewMention((thread, message) => {
21243
+ bot.onNewMention((thread, message, context) => {
21225
21244
  rehydrateAttachmentFetchers(message);
21226
- return slackRuntime.handleNewMention(thread, message);
21245
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21246
+ return slackRuntime.handleNewMention(thread, message, {
21247
+ messageContext: context
21248
+ });
21227
21249
  });
21228
- bot.onDirectMessage((thread, message) => {
21250
+ bot.onDirectMessage((thread, message, _channel, context) => {
21229
21251
  rehydrateAttachmentFetchers(message);
21230
- return slackRuntime.handleNewMention(thread, message);
21252
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21253
+ return slackRuntime.handleNewMention(thread, message, {
21254
+ messageContext: context
21255
+ });
21231
21256
  });
21232
- bot.onSubscribedMessage((thread, message) => {
21257
+ bot.onSubscribedMessage((thread, message, context) => {
21233
21258
  rehydrateAttachmentFetchers(message);
21234
- return slackRuntime.handleSubscribedMessage(thread, message);
21259
+ context?.skipped.forEach((skipped) => rehydrateAttachmentFetchers(skipped));
21260
+ return slackRuntime.handleSubscribedMessage(thread, message, {
21261
+ messageContext: context
21262
+ });
21235
21263
  });
21236
21264
  bot.onAssistantThreadStarted(
21237
21265
  (event) => slackRuntime.handleAssistantThreadStarted(event)
@@ -21718,14 +21746,13 @@ async function createApp(options) {
21718
21746
  }
21719
21747
  await next();
21720
21748
  });
21721
- app.get("/", () => GET3());
21722
- app.get("/health", () => GET2());
21723
- app.get("/api/info", () => GET());
21749
+ app.get("/", () => GET());
21750
+ app.get("/health", () => GET());
21724
21751
  app.get("/api/oauth/callback/mcp/:provider", (c) => {
21725
- return GET5(c.req.raw, c.req.param("provider"), waitUntil);
21752
+ return GET3(c.req.raw, c.req.param("provider"), waitUntil);
21726
21753
  });
21727
21754
  app.get("/api/oauth/callback/:provider", (c) => {
21728
- return GET6(c.req.raw, c.req.param("provider"), waitUntil);
21755
+ return GET4(c.req.raw, c.req.param("provider"), waitUntil);
21729
21756
  });
21730
21757
  app.post("/api/internal/turn-resume", (c) => {
21731
21758
  return POST2(c.req.raw, waitUntil);
@@ -21734,7 +21761,7 @@ async function createApp(options) {
21734
21761
  return POST(c.req.raw, waitUntil);
21735
21762
  });
21736
21763
  app.get("/api/internal/heartbeat", (c) => {
21737
- return GET4(c.req.raw, waitUntil);
21764
+ return GET2(c.req.raw, waitUntil);
21738
21765
  });
21739
21766
  app.post("/api/webhooks/:platform", (c) => {
21740
21767
  return POST3(c.req.raw, c.req.param("platform"), waitUntil);