@sentry/junior 0.84.1 → 0.86.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 (51) hide show
  1. package/dist/{agent-hooks-TFPY6V3T.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +916 -363
  3. package/dist/chat/conversation-privacy.d.ts +12 -2
  4. package/dist/chat/conversations/sql/migrations.d.ts +1 -1
  5. package/dist/chat/conversations/sql/store.d.ts +9 -1
  6. package/dist/chat/conversations/store.d.ts +13 -0
  7. package/dist/chat/current-instruction.d.ts +6 -0
  8. package/dist/chat/ingress/message-changed.d.ts +2 -0
  9. package/dist/chat/respond-helpers.d.ts +1 -2
  10. package/dist/chat/respond.d.ts +7 -1
  11. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  12. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  13. package/dist/chat/runtime/turn.d.ts +7 -0
  14. package/dist/chat/services/persist-retry.d.ts +2 -0
  15. package/dist/chat/services/turn-session-record.d.ts +31 -2
  16. package/dist/chat/slack/client.d.ts +33 -2
  17. package/dist/chat/slack/conversation-context.d.ts +15 -0
  18. package/dist/chat/state/turn-session.d.ts +9 -0
  19. package/dist/chat/task-execution/queue.d.ts +1 -1
  20. package/dist/chat/task-execution/slack-work.d.ts +10 -2
  21. package/dist/chat/task-execution/state.d.ts +44 -4
  22. package/dist/chat/task-execution/store.d.ts +21 -4
  23. package/dist/chat/task-execution/worker.d.ts +11 -4
  24. package/dist/chat/tools/slack/channel-access.d.ts +29 -0
  25. package/dist/chat/tools/slack/thread-read.d.ts +4 -1
  26. package/dist/{chunk-AL6ZFV7U.js → chunk-2NFV5FMB.js} +4 -4
  27. package/dist/{chunk-JK7376UT.js → chunk-37B2R2QJ.js} +17 -17
  28. package/dist/{chunk-QWKB6NJV.js → chunk-6I6HBOQM.js} +80 -15
  29. package/dist/{chunk-SBYMRDH7.js → chunk-6O5UI3RG.js} +1 -1
  30. package/dist/{chunk-CWMMGUWM.js → chunk-BRSQQRG6.js} +1 -1
  31. package/dist/{chunk-FPN7NYTE.js → chunk-ENPSU7L7.js} +165 -27
  32. package/dist/{chunk-T2YRJZ5A.js → chunk-FPHA6GCQ.js} +905 -711
  33. package/dist/{chunk-MJ4E2PK7.js → chunk-GB5DFM4D.js} +1 -1
  34. package/dist/{chunk-BAPRSWNW.js → chunk-GGD6WK6V.js} +124 -25
  35. package/dist/{chunk-SVFA43LT.js → chunk-JRXCSSSU.js} +19 -4
  36. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  37. package/dist/{chunk-LSG6PQF4.js → chunk-RIB3M6YA.js} +2 -2
  38. package/dist/{chunk-ALL7GG6U.js → chunk-ZU2ALUVQ.js} +45 -6
  39. package/dist/cli/chat.js +4 -4
  40. package/dist/cli/plugins.js +7 -7
  41. package/dist/cli/snapshot-warmup.js +4 -4
  42. package/dist/cli/upgrade.js +10 -7
  43. package/dist/{db-ELRCNB4A.js → db-SZVUU7RB.js} +4 -4
  44. package/dist/handlers/github-webhook/issue-comment.d.ts +3 -0
  45. package/dist/handlers/github-webhook/pull-request-review-comment.d.ts +3 -0
  46. package/dist/instrumentation.js +1 -1
  47. package/dist/nitro.js +4 -4
  48. package/dist/reporting/conversations.d.ts +2 -1
  49. package/dist/reporting.js +110 -34
  50. package/dist/{runner-2XU6QQND.js → runner-HEBRPNN2.js} +27 -13
  51. package/package.json +5 -5
@@ -10,7 +10,7 @@ import {
10
10
  isSnapshotMissingError,
11
11
  resolveRuntimeDependencySnapshot,
12
12
  runNonInteractiveCommand
13
- } from "./chunk-LSG6PQF4.js";
13
+ } from "./chunk-RIB3M6YA.js";
14
14
  import {
15
15
  pluginTaskId,
16
16
  pluginTaskParamsSchema,
@@ -22,8 +22,8 @@ import {
22
22
  buildPluginSystemPromptContributions,
23
23
  buildSystemPrompt,
24
24
  buildTurnContextPrompt,
25
- escapeXml,
26
25
  getAgentTurnSessionRecord,
26
+ getInterruptionMarker,
27
27
  loadConnectedMcpProviders,
28
28
  recordAuthorizationRequested,
29
29
  recordMcpProviderConnected,
@@ -31,7 +31,7 @@ import {
31
31
  recordSubagentStarted,
32
32
  recordToolExecutionStarted,
33
33
  upsertAgentTurnSessionRecord
34
- } from "./chunk-JK7376UT.js";
34
+ } from "./chunk-37B2R2QJ.js";
35
35
  import {
36
36
  createPluginEmbedder,
37
37
  createPluginHookRunner,
@@ -42,14 +42,15 @@ import {
42
42
  getPlugins,
43
43
  getSlackToolContext,
44
44
  resolveChannelCapabilities
45
- } from "./chunk-AL6ZFV7U.js";
45
+ } from "./chunk-2NFV5FMB.js";
46
46
  import {
47
47
  createPluginLogger,
48
48
  createPluginState
49
- } from "./chunk-CWMMGUWM.js";
49
+ } from "./chunk-BRSQQRG6.js";
50
50
  import {
51
+ getConversationStore,
51
52
  getDb
52
- } from "./chunk-QWKB6NJV.js";
53
+ } from "./chunk-6I6HBOQM.js";
53
54
  import {
54
55
  SANDBOX_DATA_ROOT,
55
56
  SANDBOX_SKILLS_ROOT,
@@ -60,7 +61,7 @@ import {
60
61
  import {
61
62
  acquireActiveLock,
62
63
  getStateAdapter
63
- } from "./chunk-MJ4E2PK7.js";
64
+ } from "./chunk-GB5DFM4D.js";
64
65
  import {
65
66
  SlackActionError,
66
67
  downloadPrivateSlackFile,
@@ -71,7 +72,7 @@ import {
71
72
  normalizeSlackConversationId,
72
73
  parseDestination,
73
74
  withSlackRetries
74
- } from "./chunk-BAPRSWNW.js";
75
+ } from "./chunk-GGD6WK6V.js";
75
76
  import {
76
77
  GEN_AI_PROVIDER_NAME,
77
78
  GEN_AI_SERVER_ADDRESS,
@@ -82,7 +83,9 @@ import {
82
83
  completeObject,
83
84
  completeText,
84
85
  encodeNonImageAttachmentForPrompt,
86
+ escapeXml,
85
87
  extractAssistantText,
88
+ extractCurrentInstructionBody,
86
89
  getGatewayApiKey,
87
90
  getPiGatewayApiKey,
88
91
  getPiMessageRole,
@@ -98,13 +101,14 @@ import {
98
101
  nextProviderRetry,
99
102
  normalizeSlackEmojiName,
100
103
  normalizeToolNameFromResult,
104
+ renderCurrentInstruction,
101
105
  resolveGatewayModel,
102
106
  stripRuntimeTurnContext,
103
107
  summarizeMessageText,
104
108
  toObservablePromptPart,
105
109
  trimTrailingAssistantMessages,
106
110
  upsertActiveSkill
107
- } from "./chunk-ALL7GG6U.js";
111
+ } from "./chunk-ZU2ALUVQ.js";
108
112
  import {
109
113
  parseSlackThreadId,
110
114
  resolveConversationPrivacy,
@@ -114,7 +118,7 @@ import {
114
118
  toGenAiPayloadMetadata,
115
119
  toGenAiPayloadTraceAttributes,
116
120
  toGenAiTextMetadata
117
- } from "./chunk-DR75T7J3.js";
121
+ } from "./chunk-L7OHKDOX.js";
118
122
  import {
119
123
  discoverSkills,
120
124
  findSkillByName,
@@ -136,6 +140,7 @@ import {
136
140
  listReferenceFiles
137
141
  } from "./chunk-6APU57E6.js";
138
142
  import {
143
+ buildTurnFailureResponse,
139
144
  extractGenAiUsageAttributes,
140
145
  extractGenAiUsageSummary,
141
146
  getActiveTraceId,
@@ -648,6 +653,16 @@ var TurnInputCommitLostError = class extends Error {
648
653
  function isTurnInputCommitLostError(error) {
649
654
  return error instanceof TurnInputCommitLostError;
650
655
  }
656
+ var TurnInputDeferredError = class extends Error {
657
+ code = "turn_input_deferred";
658
+ constructor(message = "Turn input is deferred until the active resume ends") {
659
+ super(message);
660
+ this.name = "TurnInputDeferredError";
661
+ }
662
+ };
663
+ function isTurnInputDeferredError(error) {
664
+ return error instanceof TurnInputDeferredError;
665
+ }
651
666
  function startActiveTurn(args) {
652
667
  args.conversation.processing.activeTurnId = args.nextTurnId;
653
668
  args.updateConversationStats(args.conversation);
@@ -677,184 +692,592 @@ function markTurnFailed(args) {
677
692
  args.updateConversationStats(args.conversation);
678
693
  }
679
694
 
680
- // src/chat/respond.ts
681
- import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
682
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
683
-
684
- // src/chat/capabilities/catalog.ts
685
- var cachedCatalog;
686
- function getCapabilityCatalog() {
687
- const signature = pluginCatalogRuntime.getSignature();
688
- if (cachedCatalog?.signature === signature) return cachedCatalog;
689
- const providers = pluginCatalogRuntime.getCapabilityProviders();
690
- const capabilityToProvider = /* @__PURE__ */ new Map();
691
- for (const provider of providers) {
692
- for (const capability of provider.capabilities) {
693
- if (capabilityToProvider.has(capability)) {
694
- throw new Error(
695
- `Duplicate capability registration for "${capability}"`
696
- );
695
+ // src/chat/usage.ts
696
+ var COMPONENT_USAGE_FIELDS = [
697
+ "inputTokens",
698
+ "outputTokens",
699
+ "cachedInputTokens",
700
+ "cacheCreationTokens"
701
+ ];
702
+ function hasAgentTurnUsage(usage) {
703
+ return Boolean(
704
+ usage && Object.values(usage).some(
705
+ (value) => typeof value === "number" && Number.isFinite(value)
706
+ )
707
+ );
708
+ }
709
+ function getFiniteCount(value) {
710
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
711
+ }
712
+ function getComponentTotal(usage) {
713
+ let total;
714
+ for (const field of COMPONENT_USAGE_FIELDS) {
715
+ const value = getFiniteCount(usage[field]);
716
+ if (value === void 0) continue;
717
+ total = (total ?? 0) + value;
718
+ }
719
+ return total;
720
+ }
721
+ function addAgentTurnUsage(...usages) {
722
+ const components = {};
723
+ let componentTotal;
724
+ let totalOnlyTokens;
725
+ for (const usage of usages) {
726
+ if (!usage) continue;
727
+ const usageComponentTotal = getComponentTotal(usage);
728
+ if (usageComponentTotal !== void 0) {
729
+ componentTotal = (componentTotal ?? 0) + usageComponentTotal;
730
+ for (const field of COMPONENT_USAGE_FIELDS) {
731
+ const value = getFiniteCount(usage[field]);
732
+ if (value === void 0) continue;
733
+ components[field] = (components[field] ?? 0) + value;
697
734
  }
698
- capabilityToProvider.set(capability, provider);
735
+ continue;
736
+ }
737
+ const totalTokens = getFiniteCount(usage.totalTokens);
738
+ if (totalTokens !== void 0) {
739
+ totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
699
740
  }
700
741
  }
701
- cachedCatalog = { signature, providers, capabilityToProvider };
702
- return cachedCatalog;
742
+ if (totalOnlyTokens !== void 0) {
743
+ return {
744
+ totalTokens: totalOnlyTokens + (componentTotal ?? 0)
745
+ };
746
+ }
747
+ return hasAgentTurnUsage(components) ? components : void 0;
703
748
  }
704
- var catalogLogged = false;
705
- function logCapabilityCatalogLoadedOnce() {
706
- if (catalogLogged) return;
707
- catalogLogged = true;
708
- const { providers } = getCapabilityCatalog();
709
- const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
710
- const configKeys = [
711
- ...new Set(providers.flatMap((p) => p.configKeys))
712
- ].sort();
713
- logInfo(
714
- "capability_catalog_loaded",
715
- {},
749
+
750
+ // src/chat/services/turn-session-record.ts
751
+ var AGENT_CONTINUE_MAX_SLICES = 48;
752
+ function logSessionRecordError(error, eventName, args, attributes, message) {
753
+ logException(
754
+ error,
755
+ eventName,
716
756
  {
717
- "app.capability.providers": providers.map((p) => p.provider),
718
- "app.capability.count": capabilityNames.length,
719
- "app.capability.names": capabilityNames,
720
- "app.config.key_count": configKeys.length,
721
- "app.config.keys": configKeys
757
+ slackThreadId: args.logContext.threadId,
758
+ slackUserId: args.logContext.requesterId,
759
+ slackChannelId: args.logContext.channelId,
760
+ runId: args.logContext.runId,
761
+ assistantUserName: args.logContext.assistantUserName,
762
+ modelId: args.logContext.modelId
722
763
  },
723
- "Loaded capability provider catalog"
764
+ {
765
+ "app.ai.resume_conversation_id": args.conversationId,
766
+ "app.ai.resume_session_id": args.sessionId,
767
+ ...attributes
768
+ },
769
+ message
724
770
  );
725
771
  }
726
-
727
- // src/chat/capabilities/router.ts
728
- var ProviderCredentialRouter = class {
729
- brokersByProvider;
730
- constructor(input) {
731
- this.brokersByProvider = input.brokersByProvider;
772
+ function addDurationMs(prior, current) {
773
+ const total = [prior, current].reduce((sum, value) => {
774
+ if (typeof value !== "number" || !Number.isFinite(value)) {
775
+ return sum;
776
+ }
777
+ return (sum ?? 0) + Math.max(0, Math.floor(value));
778
+ }, void 0);
779
+ return total;
780
+ }
781
+ function isContinuableBoundary(messages) {
782
+ const lastRole = getPiMessageRole(messages.at(-1));
783
+ return lastRole === "user" || lastRole === "toolResult";
784
+ }
785
+ function resumableBoundary(messages, fallbackMessages) {
786
+ const current = trimTrailingAssistantMessages(messages);
787
+ if (current.length > 0 && isContinuableBoundary(current)) {
788
+ return current;
732
789
  }
733
- async issue(input) {
734
- const broker = this.brokersByProvider[input.provider];
735
- if (!broker) {
790
+ return trimTrailingAssistantMessages(fallbackMessages ?? []);
791
+ }
792
+ async function loadTurnSessionRecord(ctx) {
793
+ const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
794
+ const existingSessionRecord = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionRecord(ctx.conversationId, ctx.sessionId) : void 0;
795
+ const hasAwaitingResumeRecord = Boolean(
796
+ existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
797
+ );
798
+ return {
799
+ canUseTurnSession,
800
+ resumedFromSessionRecord: hasAwaitingResumeRecord,
801
+ currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
802
+ existingSessionRecord
803
+ };
804
+ }
805
+ async function persistRunningSessionRecord(args) {
806
+ if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
807
+ return false;
808
+ }
809
+ try {
810
+ const latestSessionRecord = await getAgentTurnSessionRecord(
811
+ args.conversationId,
812
+ args.sessionId
813
+ );
814
+ await upsertAgentTurnSessionRecord({
815
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
816
+ conversationId: args.conversationId,
817
+ cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
818
+ cumulativeUsage: latestSessionRecord?.cumulativeUsage,
819
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
820
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
821
+ sessionId: args.sessionId,
822
+ sliceId: args.sliceId,
823
+ state: "running",
824
+ piMessages: args.messages,
825
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
826
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
827
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
828
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
829
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
830
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
831
+ } : {}
832
+ });
833
+ return true;
834
+ } catch (recordError) {
835
+ logSessionRecordError(
836
+ recordError,
837
+ "agent_turn_running_session_record_failed",
838
+ args,
839
+ {
840
+ "app.ai.resume_slice_id": args.sliceId
841
+ },
842
+ "Failed to persist running turn session record"
843
+ );
844
+ return false;
845
+ }
846
+ }
847
+ async function persistCompletedSessionRecord(args) {
848
+ let sliceId = args.sliceId;
849
+ try {
850
+ const latestSessionRecord = await getAgentTurnSessionRecord(
851
+ args.conversationId,
852
+ args.sessionId
853
+ );
854
+ sliceId = sliceId ?? latestSessionRecord?.sliceId;
855
+ if (sliceId === void 0) {
736
856
  throw new Error(
737
- `No credential broker registered for provider: ${input.provider}`
857
+ "Completed session record requires a slice id from the caller or the latest stored record"
738
858
  );
739
859
  }
740
- return await broker.issue({
741
- context: input.context,
742
- reason: input.reason
860
+ await upsertAgentTurnSessionRecord({
861
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
862
+ conversationId: args.conversationId,
863
+ cumulativeDurationMs: addDurationMs(
864
+ latestSessionRecord?.cumulativeDurationMs,
865
+ args.currentDurationMs
866
+ ),
867
+ cumulativeUsage: addAgentTurnUsage(
868
+ latestSessionRecord?.cumulativeUsage,
869
+ args.currentUsage
870
+ ),
871
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
872
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
873
+ ...args.destinationVisibility ? { destinationVisibility: args.destinationVisibility } : {},
874
+ sessionId: args.sessionId,
875
+ sliceId,
876
+ state: "completed",
877
+ piMessages: args.allMessages,
878
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
879
+ ...args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames ? {
880
+ loadedSkillNames: args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames
881
+ } : {},
882
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
883
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
884
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
885
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
886
+ } : {}
743
887
  });
888
+ } catch (recordError) {
889
+ logSessionRecordError(
890
+ recordError,
891
+ "agent_turn_completed_session_record_failed",
892
+ args,
893
+ sliceId !== void 0 ? { "app.ai.resume_slice_id": sliceId } : {},
894
+ "Failed to persist completed turn session record"
895
+ );
744
896
  }
745
- };
746
-
747
- // src/chat/credentials/user-token-store.ts
748
- import {
749
- pluginStoredTokensSchema
750
- } from "@sentry/junior-plugin-api";
751
- var storedTokensSchema = pluginStoredTokensSchema;
752
-
753
- // src/chat/credentials/state-adapter-token-store.ts
754
- var KEY_PREFIX = "oauth-token";
755
- var BUFFER_MS = 24 * 60 * 60 * 1e3;
756
- var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
757
- var REFRESH_LOCK_WAIT_MS = 3e4;
758
- var REFRESH_LOCK_RETRY_MS = 100;
759
- function tokenKey(userId, provider) {
760
- return `${KEY_PREFIX}:${userId}:${provider}`;
761
897
  }
762
- function refreshLockKey(userId, provider) {
763
- return `${tokenKey(userId, provider)}:refresh`;
898
+ async function completeDeliveredTurn(args) {
899
+ await persistCompletedSessionRecord({
900
+ channelName: args.channelName,
901
+ conversationId: args.conversationId,
902
+ currentDurationMs: args.durationMs,
903
+ currentUsage: args.usage,
904
+ destination: args.destination,
905
+ destinationVisibility: args.destinationVisibility,
906
+ source: args.source,
907
+ sessionId: args.sessionId,
908
+ sliceId: args.sliceId,
909
+ allMessages: args.messages,
910
+ loadedSkillNames: args.loadedSkillNames,
911
+ logContext: args.logContext,
912
+ requester: args.requester,
913
+ surface: args.surface,
914
+ turnStartMessageIndex: args.turnStartMessageIndex
915
+ });
764
916
  }
765
- async function sleep(ms) {
766
- await new Promise((resolve) => setTimeout(resolve, ms));
767
- }
768
- var StateAdapterTokenStore = class {
769
- state;
770
- constructor(stateAdapter) {
771
- this.state = stateAdapter;
772
- }
773
- async get(userId, provider) {
774
- const stored = await this.state.get(tokenKey(userId, provider));
775
- return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
776
- }
777
- async set(userId, provider, tokens) {
778
- const parsed = storedTokensSchema.parse(tokens);
779
- const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
780
- const ttlMs2 = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
781
- await this.state.set(tokenKey(userId, provider), parsed, ttlMs2);
782
- }
783
- async delete(userId, provider) {
784
- await this.state.delete(tokenKey(userId, provider));
785
- }
786
- /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
787
- async withRefresh(userId, provider, callback) {
788
- const lockKey = refreshLockKey(userId, provider);
789
- const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
790
- while (true) {
791
- const lock = await acquireActiveLock(this.state, lockKey);
792
- if (lock) {
793
- try {
794
- return await callback();
795
- } finally {
796
- await this.state.releaseLock(lock);
797
- }
798
- }
799
- if (Date.now() >= deadline) {
800
- throw new Error(`Could not acquire OAuth token refresh lock`);
801
- }
802
- await sleep(REFRESH_LOCK_RETRY_MS);
917
+ async function persistAuthPauseSessionRecord(args) {
918
+ const nextSliceId = args.currentSliceId + 1;
919
+ try {
920
+ const latestSessionRecord = await getAgentTurnSessionRecord(
921
+ args.conversationId,
922
+ args.sessionId
923
+ );
924
+ const piMessages = resumableBoundary(
925
+ args.messages,
926
+ latestSessionRecord?.piMessages
927
+ );
928
+ if (piMessages.length > 0 && !isContinuableBoundary(piMessages)) {
929
+ return void 0;
803
930
  }
931
+ return await upsertAgentTurnSessionRecord({
932
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
933
+ conversationId: args.conversationId,
934
+ cumulativeDurationMs: addDurationMs(
935
+ latestSessionRecord?.cumulativeDurationMs,
936
+ args.currentDurationMs
937
+ ),
938
+ cumulativeUsage: addAgentTurnUsage(
939
+ latestSessionRecord?.cumulativeUsage,
940
+ args.currentUsage
941
+ ),
942
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
943
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
944
+ sessionId: args.sessionId,
945
+ sliceId: nextSliceId,
946
+ state: "awaiting_resume",
947
+ piMessages,
948
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
949
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
950
+ resumeReason: "auth",
951
+ resumedFromSliceId: args.currentSliceId,
952
+ errorMessage: args.errorMessage,
953
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
954
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
955
+ });
956
+ } catch (recordError) {
957
+ logSessionRecordError(
958
+ recordError,
959
+ "agent_turn_auth_resume_session_record_failed",
960
+ args,
961
+ {
962
+ "app.ai.resume_from_slice_id": args.currentSliceId,
963
+ "app.ai.resume_next_slice_id": nextSliceId
964
+ },
965
+ "Failed to persist auth session record before retry"
966
+ );
804
967
  }
805
- };
806
-
807
- // src/chat/capabilities/factory.ts
808
- var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
809
- function createUserTokenStore() {
810
- return new StateAdapterTokenStore(getStateAdapter());
968
+ return void 0;
811
969
  }
812
- function createProviderCredentialRouter(userTokenStore) {
813
- logCapabilityCatalogLoadedOnce();
814
- const brokersByProvider = {};
815
- for (const plugin of pluginCatalogRuntime.getProviders()) {
816
- const { name } = plugin.manifest;
817
- if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
818
- continue;
970
+ async function persistTimeoutSessionRecord(args) {
971
+ const nextSliceId = args.currentSliceId + 1;
972
+ try {
973
+ const latestSessionRecord = await getAgentTurnSessionRecord(
974
+ args.conversationId,
975
+ args.sessionId
976
+ );
977
+ const piMessages = resumableBoundary(
978
+ args.messages,
979
+ latestSessionRecord?.piMessages
980
+ );
981
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
982
+ return void 0;
819
983
  }
820
- brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
821
- userTokenStore
984
+ const cumulativeDurationMs = addDurationMs(
985
+ latestSessionRecord?.cumulativeDurationMs,
986
+ args.currentDurationMs
987
+ );
988
+ const cumulativeUsage = addAgentTurnUsage(
989
+ latestSessionRecord?.cumulativeUsage,
990
+ args.currentUsage
991
+ );
992
+ if (nextSliceId > AGENT_CONTINUE_MAX_SLICES) {
993
+ return await upsertAgentTurnSessionRecord({
994
+ ...args.channelName ?? latestSessionRecord?.channelName ? {
995
+ channelName: args.channelName ?? latestSessionRecord?.channelName
996
+ } : {},
997
+ conversationId: args.conversationId,
998
+ cumulativeDurationMs,
999
+ cumulativeUsage,
1000
+ ...args.destination ?? latestSessionRecord?.destination ? {
1001
+ destination: args.destination ?? latestSessionRecord?.destination
1002
+ } : {},
1003
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1004
+ sessionId: args.sessionId,
1005
+ sliceId: args.currentSliceId,
1006
+ state: "failed",
1007
+ piMessages,
1008
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1009
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1010
+ resumeReason: "timeout",
1011
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1012
+ errorMessage: `Agent continuation exceeded slice limit (${AGENT_CONTINUE_MAX_SLICES})`,
1013
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1014
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1015
+ });
1016
+ }
1017
+ return await upsertAgentTurnSessionRecord({
1018
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1019
+ conversationId: args.conversationId,
1020
+ cumulativeDurationMs,
1021
+ cumulativeUsage,
1022
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1023
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1024
+ sessionId: args.sessionId,
1025
+ sliceId: nextSliceId,
1026
+ state: "awaiting_resume",
1027
+ piMessages,
1028
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1029
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1030
+ resumeReason: "timeout",
1031
+ resumedFromSliceId: args.currentSliceId,
1032
+ errorMessage: args.errorMessage,
1033
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1034
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
822
1035
  });
1036
+ } catch (recordError) {
1037
+ logSessionRecordError(
1038
+ recordError,
1039
+ "agent_continue_session_record_failed",
1040
+ args,
1041
+ {
1042
+ "app.ai.resume_from_slice_id": args.currentSliceId,
1043
+ "app.ai.resume_next_slice_id": nextSliceId
1044
+ },
1045
+ "Failed to persist session record before scheduling agent continuation"
1046
+ );
1047
+ return void 0;
823
1048
  }
824
- return new ProviderCredentialRouter({ brokersByProvider });
825
1049
  }
826
- function getSandboxEgressRouter() {
827
- const stateAdapter = getStateAdapter();
828
- let router = sandboxEgressRouters.get(stateAdapter);
829
- if (!router) {
830
- router = createProviderCredentialRouter(
831
- new StateAdapterTokenStore(stateAdapter)
1050
+ async function persistYieldSessionRecord(args) {
1051
+ try {
1052
+ const latestSessionRecord = await getAgentTurnSessionRecord(
1053
+ args.conversationId,
1054
+ args.sessionId
832
1055
  );
833
- sandboxEgressRouters.set(stateAdapter, router);
1056
+ const piMessages = resumableBoundary(
1057
+ args.messages,
1058
+ latestSessionRecord?.piMessages
1059
+ );
1060
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
1061
+ return void 0;
1062
+ }
1063
+ return await upsertAgentTurnSessionRecord({
1064
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1065
+ conversationId: args.conversationId,
1066
+ cumulativeDurationMs: addDurationMs(
1067
+ latestSessionRecord?.cumulativeDurationMs,
1068
+ args.currentDurationMs
1069
+ ),
1070
+ cumulativeUsage: addAgentTurnUsage(
1071
+ latestSessionRecord?.cumulativeUsage,
1072
+ args.currentUsage
1073
+ ),
1074
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1075
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1076
+ sessionId: args.sessionId,
1077
+ sliceId: args.currentSliceId,
1078
+ state: "awaiting_resume",
1079
+ piMessages,
1080
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1081
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1082
+ resumeReason: "yield",
1083
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1084
+ errorMessage: args.errorMessage,
1085
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1086
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1087
+ });
1088
+ } catch (recordError) {
1089
+ logSessionRecordError(
1090
+ recordError,
1091
+ "agent_turn_yield_session_record_failed",
1092
+ args,
1093
+ {
1094
+ "app.ai.resume_slice_id": args.currentSliceId
1095
+ },
1096
+ "Failed to persist cooperative yield session record"
1097
+ );
1098
+ return void 0;
834
1099
  }
835
- return router;
836
- }
837
- async function issueProviderCredentialLease(input) {
838
- return await getSandboxEgressRouter().issue(input);
839
1100
  }
840
1101
 
841
- // src/chat/capabilities/jr-rpc-command.ts
842
- import { Bash, defineCommand } from "just-bash";
843
- function commandResult(input) {
844
- let stdout = "";
845
- if (typeof input.stdout === "string") {
846
- stdout = input.stdout;
847
- } else if (input.stdout !== void 0) {
848
- stdout = `${JSON.stringify(input.stdout, null, 2)}
849
- `;
850
- }
851
- return {
852
- stdout,
853
- stderr: input.stderr ?? "",
854
- exitCode: input.exitCode
855
- };
856
- }
857
- function requireChannelConfiguration(deps) {
1102
+ // src/chat/respond.ts
1103
+ import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
1104
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
1105
+ import { z as z4 } from "zod";
1106
+
1107
+ // src/chat/capabilities/catalog.ts
1108
+ var cachedCatalog;
1109
+ function getCapabilityCatalog() {
1110
+ const signature = pluginCatalogRuntime.getSignature();
1111
+ if (cachedCatalog?.signature === signature) return cachedCatalog;
1112
+ const providers = pluginCatalogRuntime.getCapabilityProviders();
1113
+ const capabilityToProvider = /* @__PURE__ */ new Map();
1114
+ for (const provider of providers) {
1115
+ for (const capability of provider.capabilities) {
1116
+ if (capabilityToProvider.has(capability)) {
1117
+ throw new Error(
1118
+ `Duplicate capability registration for "${capability}"`
1119
+ );
1120
+ }
1121
+ capabilityToProvider.set(capability, provider);
1122
+ }
1123
+ }
1124
+ cachedCatalog = { signature, providers, capabilityToProvider };
1125
+ return cachedCatalog;
1126
+ }
1127
+ var catalogLogged = false;
1128
+ function logCapabilityCatalogLoadedOnce() {
1129
+ if (catalogLogged) return;
1130
+ catalogLogged = true;
1131
+ const { providers } = getCapabilityCatalog();
1132
+ const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
1133
+ const configKeys = [
1134
+ ...new Set(providers.flatMap((p) => p.configKeys))
1135
+ ].sort();
1136
+ logInfo(
1137
+ "capability_catalog_loaded",
1138
+ {},
1139
+ {
1140
+ "app.capability.providers": providers.map((p) => p.provider),
1141
+ "app.capability.count": capabilityNames.length,
1142
+ "app.capability.names": capabilityNames,
1143
+ "app.config.key_count": configKeys.length,
1144
+ "app.config.keys": configKeys
1145
+ },
1146
+ "Loaded capability provider catalog"
1147
+ );
1148
+ }
1149
+
1150
+ // src/chat/capabilities/router.ts
1151
+ var ProviderCredentialRouter = class {
1152
+ brokersByProvider;
1153
+ constructor(input) {
1154
+ this.brokersByProvider = input.brokersByProvider;
1155
+ }
1156
+ async issue(input) {
1157
+ const broker = this.brokersByProvider[input.provider];
1158
+ if (!broker) {
1159
+ throw new Error(
1160
+ `No credential broker registered for provider: ${input.provider}`
1161
+ );
1162
+ }
1163
+ return await broker.issue({
1164
+ context: input.context,
1165
+ reason: input.reason
1166
+ });
1167
+ }
1168
+ };
1169
+
1170
+ // src/chat/credentials/user-token-store.ts
1171
+ import {
1172
+ pluginStoredTokensSchema
1173
+ } from "@sentry/junior-plugin-api";
1174
+ var storedTokensSchema = pluginStoredTokensSchema;
1175
+
1176
+ // src/chat/credentials/state-adapter-token-store.ts
1177
+ var KEY_PREFIX = "oauth-token";
1178
+ var BUFFER_MS = 24 * 60 * 60 * 1e3;
1179
+ var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
1180
+ var REFRESH_LOCK_WAIT_MS = 3e4;
1181
+ var REFRESH_LOCK_RETRY_MS = 100;
1182
+ function tokenKey(userId, provider) {
1183
+ return `${KEY_PREFIX}:${userId}:${provider}`;
1184
+ }
1185
+ function refreshLockKey(userId, provider) {
1186
+ return `${tokenKey(userId, provider)}:refresh`;
1187
+ }
1188
+ async function sleep(ms) {
1189
+ await new Promise((resolve) => setTimeout(resolve, ms));
1190
+ }
1191
+ var StateAdapterTokenStore = class {
1192
+ state;
1193
+ constructor(stateAdapter) {
1194
+ this.state = stateAdapter;
1195
+ }
1196
+ async get(userId, provider) {
1197
+ const stored = await this.state.get(tokenKey(userId, provider));
1198
+ return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
1199
+ }
1200
+ async set(userId, provider, tokens) {
1201
+ const parsed = storedTokensSchema.parse(tokens);
1202
+ const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
1203
+ const ttlMs2 = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
1204
+ await this.state.set(tokenKey(userId, provider), parsed, ttlMs2);
1205
+ }
1206
+ async delete(userId, provider) {
1207
+ await this.state.delete(tokenKey(userId, provider));
1208
+ }
1209
+ /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
1210
+ async withRefresh(userId, provider, callback) {
1211
+ const lockKey = refreshLockKey(userId, provider);
1212
+ const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
1213
+ while (true) {
1214
+ const lock = await acquireActiveLock(this.state, lockKey);
1215
+ if (lock) {
1216
+ try {
1217
+ return await callback();
1218
+ } finally {
1219
+ await this.state.releaseLock(lock);
1220
+ }
1221
+ }
1222
+ if (Date.now() >= deadline) {
1223
+ throw new Error(`Could not acquire OAuth token refresh lock`);
1224
+ }
1225
+ await sleep(REFRESH_LOCK_RETRY_MS);
1226
+ }
1227
+ }
1228
+ };
1229
+
1230
+ // src/chat/capabilities/factory.ts
1231
+ var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
1232
+ function createUserTokenStore() {
1233
+ return new StateAdapterTokenStore(getStateAdapter());
1234
+ }
1235
+ function createProviderCredentialRouter(userTokenStore) {
1236
+ logCapabilityCatalogLoadedOnce();
1237
+ const brokersByProvider = {};
1238
+ for (const plugin of pluginCatalogRuntime.getProviders()) {
1239
+ const { name } = plugin.manifest;
1240
+ if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
1241
+ continue;
1242
+ }
1243
+ brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
1244
+ userTokenStore
1245
+ });
1246
+ }
1247
+ return new ProviderCredentialRouter({ brokersByProvider });
1248
+ }
1249
+ function getSandboxEgressRouter() {
1250
+ const stateAdapter = getStateAdapter();
1251
+ let router = sandboxEgressRouters.get(stateAdapter);
1252
+ if (!router) {
1253
+ router = createProviderCredentialRouter(
1254
+ new StateAdapterTokenStore(stateAdapter)
1255
+ );
1256
+ sandboxEgressRouters.set(stateAdapter, router);
1257
+ }
1258
+ return router;
1259
+ }
1260
+ async function issueProviderCredentialLease(input) {
1261
+ return await getSandboxEgressRouter().issue(input);
1262
+ }
1263
+
1264
+ // src/chat/capabilities/jr-rpc-command.ts
1265
+ import { Bash, defineCommand } from "just-bash";
1266
+ function commandResult(input) {
1267
+ let stdout = "";
1268
+ if (typeof input.stdout === "string") {
1269
+ stdout = input.stdout;
1270
+ } else if (input.stdout !== void 0) {
1271
+ stdout = `${JSON.stringify(input.stdout, null, 2)}
1272
+ `;
1273
+ }
1274
+ return {
1275
+ stdout,
1276
+ stderr: input.stderr ?? "",
1277
+ exitCode: input.exitCode
1278
+ };
1279
+ }
1280
+ function requireChannelConfiguration(deps) {
858
1281
  if (deps.channelConfiguration) {
859
1282
  return { ok: true, configuration: deps.channelConfiguration };
860
1283
  }
@@ -4467,6 +4890,7 @@ async function getPermalinkBestEffort(args) {
4467
4890
  3,
4468
4891
  {
4469
4892
  action: "chat.getPermalink",
4893
+ idempotent: true,
4470
4894
  spanAttributes: {
4471
4895
  "app.slack.channel_id": args.channelId,
4472
4896
  "app.slack.message_ts": args.messageTs
@@ -4536,6 +4960,7 @@ async function deleteSlackMessage(input) {
4536
4960
  3,
4537
4961
  {
4538
4962
  action: "chat.delete",
4963
+ idempotent: true,
4539
4964
  spanAttributes: {
4540
4965
  "app.slack.channel_id": channelId,
4541
4966
  "app.slack.message_ts": timestamp
@@ -4644,6 +5069,7 @@ async function addReactionToMessage(input) {
4644
5069
  3,
4645
5070
  {
4646
5071
  action: "reactions.add",
5072
+ idempotent: true,
4647
5073
  spanAttributes: {
4648
5074
  "app.slack.channel_id": channelId,
4649
5075
  "app.slack.message_ts": timestamp,
@@ -4682,6 +5108,7 @@ async function removeReactionFromMessage(input) {
4682
5108
  3,
4683
5109
  {
4684
5110
  action: "reactions.remove",
5111
+ idempotent: true,
4685
5112
  spanAttributes: {
4686
5113
  "app.slack.channel_id": channelId,
4687
5114
  "app.slack.message_ts": timestamp,
@@ -5747,6 +6174,31 @@ function createSlackListUpdateItemTool(state) {
5747
6174
  // src/chat/tools/slack/thread-read.ts
5748
6175
  import { Type as Type19 } from "@sinclair/typebox";
5749
6176
 
6177
+ // src/chat/tools/slack/channel-access.ts
6178
+ async function checkSlackChannelReadAccess(args) {
6179
+ const target = normalizeSlackConversationId(args.targetChannelId);
6180
+ if (!target) {
6181
+ return { allowed: false, error: "Invalid Slack channel ID." };
6182
+ }
6183
+ const currentChannels = args.currentChannelIds.map((channelId) => normalizeSlackConversationId(channelId)).filter((channelId) => Boolean(channelId));
6184
+ if (currentChannels.includes(target)) {
6185
+ return { allowed: true };
6186
+ }
6187
+ const store = args.store ?? getConversationStore();
6188
+ const visibility = await store.getDestinationVisibility({
6189
+ provider: "slack",
6190
+ providerTenantId: args.teamId,
6191
+ providerDestinationId: target
6192
+ });
6193
+ if (visibility === "public") {
6194
+ return { allowed: true };
6195
+ }
6196
+ return {
6197
+ allowed: false,
6198
+ error: "Cannot read this Slack conversation: only the current conversation or public channels Junior has seen in this workspace are readable."
6199
+ };
6200
+ }
6201
+
5750
6202
  // src/chat/tools/slack/slack-message-url.ts
5751
6203
  var SLACK_HOST_PATTERN = /^[a-z0-9-]+\.slack(?:-gov)?\.com$/;
5752
6204
  var ARCHIVE_PATH_PATTERN = /^\/archives\/([CDG][A-Z0-9]+)\/p(\d{10})(\d{6})$/;
@@ -5906,26 +6358,9 @@ function truncateMessages(messages, maxChars) {
5906
6358
  }
5907
6359
  return { messages: kept, omitted: messages.length - kept.length };
5908
6360
  }
5909
- function checkChannelAccess(targetChannelId, currentChannelId) {
5910
- const target = normalizeSlackConversationId(targetChannelId);
5911
- const current = normalizeSlackConversationId(currentChannelId);
5912
- if (!target) {
5913
- return { allowed: false, error: "Invalid Slack channel ID." };
5914
- }
5915
- if (target.startsWith("C")) {
5916
- return { allowed: true };
5917
- }
5918
- if (target === current) {
5919
- return { allowed: true };
5920
- }
5921
- return {
5922
- allowed: false,
5923
- error: "Cannot read private channels or DMs unless the link is from the current conversation."
5924
- };
5925
- }
5926
- function createSlackThreadReadTool(context) {
6361
+ function createSlackThreadReadTool(context, deps = {}) {
5927
6362
  return tool({
5928
- description: "Read a Slack thread from a shared Slack message archive URL or explicit channel + timestamp. Use when the user shares a Slack message link (https://*.slack.com/archives/...) and you need the referenced message and its thread context. Public channel links can be read if the bot has access; private channels and DMs are only readable when they are the current conversation.",
6363
+ description: "Read a Slack thread from a shared Slack message archive URL or explicit channel + timestamp. Use when the user shares a Slack message link (https://*.slack.com/archives/...) and you need the referenced message and its thread context. Only the current conversation and public channels Junior has seen in this workspace are readable.",
5929
6364
  annotations: { readOnlyHint: true, destructiveHint: false },
5930
6365
  inputSchema: Type19.Object({
5931
6366
  url: Type19.Optional(
@@ -5985,10 +6420,15 @@ function createSlackThreadReadTool(context) {
5985
6420
  error: "Provide either a Slack message `url` or both `channel_id` and `ts`."
5986
6421
  };
5987
6422
  }
5988
- const access = checkChannelAccess(
5989
- channelId,
5990
- context.destinationChannelId
5991
- );
6423
+ const access = await checkSlackChannelReadAccess({
6424
+ currentChannelIds: [
6425
+ context.destinationChannelId,
6426
+ context.sourceChannelId
6427
+ ],
6428
+ store: deps.visibilityStore,
6429
+ targetChannelId: channelId,
6430
+ teamId: context.teamId
6431
+ });
5992
6432
  if (!access.allowed) {
5993
6433
  return {
5994
6434
  ok: false,
@@ -10862,11 +11302,7 @@ function buildClassifierPrompt(args) {
10862
11302
  );
10863
11303
  }
10864
11304
  }
10865
- sections.push(
10866
- "<current-instruction>",
10867
- args.messageText.trim() || "[empty]",
10868
- "</current-instruction>"
10869
- );
11305
+ sections.push(renderCurrentInstruction(args.messageText.trim() || "[empty]"));
10870
11306
  for (const block of args.currentTurnBlocks ?? []) {
10871
11307
  const trimmed = block.trim();
10872
11308
  if (!trimmed) {
@@ -10875,499 +11311,119 @@ function buildClassifierPrompt(args) {
10875
11311
  sections.push("", trimmed);
10876
11312
  }
10877
11313
  return sections.join("\n");
10878
- }
10879
- async function selectTurnThinkingLevel(args) {
10880
- const trimmedContext = trimContextForRouter(args.conversationContext);
10881
- const instructionLength = args.messageText.trim().length;
10882
- const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
10883
- (block) => block.trim().length > 0
10884
- ).length;
10885
- const prompt = buildClassifierPrompt({
10886
- conversationContext: trimmedContext,
10887
- currentTurnBlocks: args.currentTurnBlocks,
10888
- messageText: args.messageText
10889
- });
10890
- const logContext = {
10891
- slackThreadId: args.context?.threadId,
10892
- slackChannelId: args.context?.channelId,
10893
- slackUserId: args.context?.requesterId,
10894
- runId: args.context?.runId,
10895
- modelId: args.fastModelId
10896
- };
10897
- return withSpan(
10898
- "chat.route_thinking",
10899
- "chat.route_thinking",
10900
- logContext,
10901
- async () => {
10902
- setSpanAttributes({
10903
- "app.ai.router.prompt_char_count": prompt.length,
10904
- "app.ai.router.instruction_char_count": instructionLength,
10905
- "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
10906
- "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
10907
- "app.ai.router.turn_block_count": turnBlockCount
10908
- });
10909
- const selection = await classifyTurn({
10910
- completeObject: args.completeObject,
10911
- fastModelId: args.fastModelId,
10912
- metadata: {
10913
- modelId: args.fastModelId,
10914
- threadId: args.context?.threadId ?? "",
10915
- channelId: args.context?.channelId ?? "",
10916
- requesterId: args.context?.requesterId ?? "",
10917
- runId: args.context?.runId ?? ""
10918
- },
10919
- prompt
10920
- });
10921
- const normalizedSelection = applyThinkingFloor(selection, {
10922
- minimum: trimmedContext || turnBlockCount > 0 ? "medium" : void 0
10923
- });
10924
- setSpanAttributes({
10925
- "app.ai.thinking_level": normalizedSelection.thinkingLevel,
10926
- "app.ai.thinking_level_reason": normalizedSelection.reason,
10927
- ...normalizedSelection.confidence !== void 0 ? {
10928
- "app.ai.thinking_level_confidence": normalizedSelection.confidence
10929
- } : {}
10930
- });
10931
- return normalizedSelection;
10932
- }
10933
- );
10934
- }
10935
- function applyThinkingFloor(selection, args) {
10936
- const minimum = args.minimum;
10937
- if (!minimum || selection.thinkingLevel === "none" || THINKING_LEVEL_RANK[selection.thinkingLevel] >= THINKING_LEVEL_RANK[minimum]) {
10938
- return selection;
10939
- }
10940
- return {
10941
- ...selection,
10942
- thinkingLevel: minimum,
10943
- reason: `thinking_floor:${minimum}:${selection.reason}`
10944
- };
10945
- }
10946
- async function classifyTurn(args) {
10947
- try {
10948
- const result = await args.completeObject({
10949
- modelId: args.fastModelId,
10950
- schema: turnExecutionProfileSchema,
10951
- maxTokens: 120,
10952
- metadata: args.metadata,
10953
- prompt: args.prompt,
10954
- thinkingLevel: "low",
10955
- system: buildClassifierSystemPrompt(),
10956
- temperature: 0
10957
- });
10958
- const parsed = turnExecutionProfileSchema.parse(result.object);
10959
- const reason = parsed.reason.trim();
10960
- if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
10961
- return {
10962
- confidence: parsed.confidence,
10963
- thinkingLevel: DEFAULT_THINKING_LEVEL,
10964
- reason: `low_confidence_medium_default:${reason}`
10965
- };
10966
- }
10967
- return {
10968
- confidence: parsed.confidence,
10969
- thinkingLevel: parsed.thinking_level,
10970
- reason
10971
- };
10972
- } catch {
10973
- return {
10974
- thinkingLevel: DEFAULT_THINKING_LEVEL,
10975
- reason: "classifier_error_default"
10976
- };
10977
- }
10978
- }
10979
- function toAgentThinkingLevel(level) {
10980
- switch (level) {
10981
- case "none":
10982
- return "off";
10983
- case "low":
10984
- return "low";
10985
- case "medium":
10986
- return "medium";
10987
- case "high":
10988
- return "high";
10989
- case "xhigh":
10990
- return "xhigh";
10991
- }
10992
- }
10993
-
10994
- // src/chat/usage.ts
10995
- var COMPONENT_USAGE_FIELDS = [
10996
- "inputTokens",
10997
- "outputTokens",
10998
- "cachedInputTokens",
10999
- "cacheCreationTokens"
11000
- ];
11001
- function hasAgentTurnUsage(usage) {
11002
- return Boolean(
11003
- usage && Object.values(usage).some(
11004
- (value) => typeof value === "number" && Number.isFinite(value)
11005
- )
11006
- );
11007
- }
11008
- function getFiniteCount(value) {
11009
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
11010
- }
11011
- function getComponentTotal(usage) {
11012
- let total;
11013
- for (const field of COMPONENT_USAGE_FIELDS) {
11014
- const value = getFiniteCount(usage[field]);
11015
- if (value === void 0) continue;
11016
- total = (total ?? 0) + value;
11017
- }
11018
- return total;
11019
- }
11020
- function addAgentTurnUsage(...usages) {
11021
- const components = {};
11022
- let componentTotal;
11023
- let totalOnlyTokens;
11024
- for (const usage of usages) {
11025
- if (!usage) continue;
11026
- const usageComponentTotal = getComponentTotal(usage);
11027
- if (usageComponentTotal !== void 0) {
11028
- componentTotal = (componentTotal ?? 0) + usageComponentTotal;
11029
- for (const field of COMPONENT_USAGE_FIELDS) {
11030
- const value = getFiniteCount(usage[field]);
11031
- if (value === void 0) continue;
11032
- components[field] = (components[field] ?? 0) + value;
11033
- }
11034
- continue;
11035
- }
11036
- const totalTokens = getFiniteCount(usage.totalTokens);
11037
- if (totalTokens !== void 0) {
11038
- totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
11039
- }
11040
- }
11041
- if (totalOnlyTokens !== void 0) {
11042
- return {
11043
- totalTokens: totalOnlyTokens + (componentTotal ?? 0)
11044
- };
11045
- }
11046
- return hasAgentTurnUsage(components) ? components : void 0;
11047
- }
11048
-
11049
- // src/chat/services/turn-session-record.ts
11050
- var AGENT_CONTINUE_MAX_SLICES = 48;
11051
- function logSessionRecordError(error, eventName, args, attributes, message) {
11052
- logException(
11053
- error,
11054
- eventName,
11055
- {
11056
- slackThreadId: args.logContext.threadId,
11057
- slackUserId: args.logContext.requesterId,
11058
- slackChannelId: args.logContext.channelId,
11059
- runId: args.logContext.runId,
11060
- assistantUserName: args.logContext.assistantUserName,
11061
- modelId: args.logContext.modelId
11062
- },
11063
- {
11064
- "app.ai.resume_conversation_id": args.conversationId,
11065
- "app.ai.resume_session_id": args.sessionId,
11066
- ...attributes
11067
- },
11068
- message
11069
- );
11070
- }
11071
- function addDurationMs(prior, current) {
11072
- const total = [prior, current].reduce((sum, value) => {
11073
- if (typeof value !== "number" || !Number.isFinite(value)) {
11074
- return sum;
11075
- }
11076
- return (sum ?? 0) + Math.max(0, Math.floor(value));
11077
- }, void 0);
11078
- return total;
11079
- }
11080
- function isContinuableBoundary(messages) {
11081
- const lastRole = getPiMessageRole(messages.at(-1));
11082
- return lastRole === "user" || lastRole === "toolResult";
11083
- }
11084
- function resumableBoundary(messages, fallbackMessages) {
11085
- const current = trimTrailingAssistantMessages(messages);
11086
- if (current.length > 0 && isContinuableBoundary(current)) {
11087
- return current;
11088
- }
11089
- return trimTrailingAssistantMessages(fallbackMessages ?? []);
11090
- }
11091
- async function loadTurnSessionRecord(ctx) {
11092
- const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
11093
- const existingSessionRecord = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionRecord(ctx.conversationId, ctx.sessionId) : void 0;
11094
- const hasAwaitingResumeRecord = Boolean(
11095
- existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
11096
- );
11097
- return {
11098
- canUseTurnSession,
11099
- resumedFromSessionRecord: hasAwaitingResumeRecord,
11100
- currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
11101
- existingSessionRecord
11102
- };
11103
- }
11104
- async function persistRunningSessionRecord(args) {
11105
- if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
11106
- return false;
11107
- }
11108
- try {
11109
- const latestSessionRecord = await getAgentTurnSessionRecord(
11110
- args.conversationId,
11111
- args.sessionId
11112
- );
11113
- await upsertAgentTurnSessionRecord({
11114
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11115
- conversationId: args.conversationId,
11116
- cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
11117
- cumulativeUsage: latestSessionRecord?.cumulativeUsage,
11118
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11119
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11120
- sessionId: args.sessionId,
11121
- sliceId: args.sliceId,
11122
- state: "running",
11123
- piMessages: args.messages,
11124
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11125
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11126
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11127
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
11128
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
11129
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
11130
- } : {}
11131
- });
11132
- return true;
11133
- } catch (recordError) {
11134
- logSessionRecordError(
11135
- recordError,
11136
- "agent_turn_running_session_record_failed",
11137
- args,
11138
- {
11139
- "app.ai.resume_slice_id": args.sliceId
11140
- },
11141
- "Failed to persist running turn session record"
11142
- );
11143
- return false;
11144
- }
11145
- }
11146
- async function persistCompletedSessionRecord(args) {
11147
- try {
11148
- const latestSessionRecord = await getAgentTurnSessionRecord(
11149
- args.conversationId,
11150
- args.sessionId
11151
- );
11152
- await upsertAgentTurnSessionRecord({
11153
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11154
- conversationId: args.conversationId,
11155
- cumulativeDurationMs: addDurationMs(
11156
- latestSessionRecord?.cumulativeDurationMs,
11157
- args.currentDurationMs
11158
- ),
11159
- cumulativeUsage: addAgentTurnUsage(
11160
- latestSessionRecord?.cumulativeUsage,
11161
- args.currentUsage
11162
- ),
11163
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11164
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11165
- sessionId: args.sessionId,
11166
- sliceId: args.sliceId,
11167
- state: "completed",
11168
- piMessages: args.allMessages,
11169
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11170
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11171
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11172
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
11173
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
11174
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
11175
- } : {}
11176
- });
11177
- } catch (recordError) {
11178
- logSessionRecordError(
11179
- recordError,
11180
- "agent_turn_completed_session_record_failed",
11181
- args,
11182
- {
11183
- "app.ai.resume_slice_id": args.sliceId
11184
- },
11185
- "Failed to persist completed turn session record"
11186
- );
11187
- }
11188
- }
11189
- async function persistAuthPauseSessionRecord(args) {
11190
- const nextSliceId = args.currentSliceId + 1;
11191
- try {
11192
- const latestSessionRecord = await getAgentTurnSessionRecord(
11193
- args.conversationId,
11194
- args.sessionId
11195
- );
11196
- const piMessages = resumableBoundary(
11197
- args.messages,
11198
- latestSessionRecord?.piMessages
11199
- );
11200
- if (piMessages.length > 0 && !isContinuableBoundary(piMessages)) {
11201
- return void 0;
11202
- }
11203
- return await upsertAgentTurnSessionRecord({
11204
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11205
- conversationId: args.conversationId,
11206
- cumulativeDurationMs: addDurationMs(
11207
- latestSessionRecord?.cumulativeDurationMs,
11208
- args.currentDurationMs
11209
- ),
11210
- cumulativeUsage: addAgentTurnUsage(
11211
- latestSessionRecord?.cumulativeUsage,
11212
- args.currentUsage
11213
- ),
11214
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11215
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11216
- sessionId: args.sessionId,
11217
- sliceId: nextSliceId,
11218
- state: "awaiting_resume",
11219
- piMessages,
11220
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11221
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11222
- resumeReason: "auth",
11223
- resumedFromSliceId: args.currentSliceId,
11224
- errorMessage: args.errorMessage,
11225
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11226
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11227
- });
11228
- } catch (recordError) {
11229
- logSessionRecordError(
11230
- recordError,
11231
- "agent_turn_auth_resume_session_record_failed",
11232
- args,
11233
- {
11234
- "app.ai.resume_from_slice_id": args.currentSliceId,
11235
- "app.ai.resume_next_slice_id": nextSliceId
11236
- },
11237
- "Failed to persist auth session record before retry"
11238
- );
11239
- }
11240
- return void 0;
11241
- }
11242
- async function persistTimeoutSessionRecord(args) {
11243
- const nextSliceId = args.currentSliceId + 1;
11244
- try {
11245
- const latestSessionRecord = await getAgentTurnSessionRecord(
11246
- args.conversationId,
11247
- args.sessionId
11248
- );
11249
- const piMessages = resumableBoundary(
11250
- args.messages,
11251
- latestSessionRecord?.piMessages
11252
- );
11253
- if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11254
- return void 0;
11255
- }
11256
- const cumulativeDurationMs = addDurationMs(
11257
- latestSessionRecord?.cumulativeDurationMs,
11258
- args.currentDurationMs
11259
- );
11260
- const cumulativeUsage = addAgentTurnUsage(
11261
- latestSessionRecord?.cumulativeUsage,
11262
- args.currentUsage
11263
- );
11264
- if (nextSliceId > AGENT_CONTINUE_MAX_SLICES) {
11265
- return await upsertAgentTurnSessionRecord({
11266
- ...args.channelName ?? latestSessionRecord?.channelName ? {
11267
- channelName: args.channelName ?? latestSessionRecord?.channelName
11268
- } : {},
11269
- conversationId: args.conversationId,
11270
- cumulativeDurationMs,
11271
- cumulativeUsage,
11272
- ...args.destination ?? latestSessionRecord?.destination ? {
11273
- destination: args.destination ?? latestSessionRecord?.destination
11274
- } : {},
11275
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11276
- sessionId: args.sessionId,
11277
- sliceId: args.currentSliceId,
11278
- state: "failed",
11279
- piMessages,
11280
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11281
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11282
- resumeReason: "timeout",
11283
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
11284
- errorMessage: `Agent continuation exceeded slice limit (${AGENT_CONTINUE_MAX_SLICES})`,
11285
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11286
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11314
+ }
11315
+ async function selectTurnThinkingLevel(args) {
11316
+ const trimmedContext = trimContextForRouter(args.conversationContext);
11317
+ const instructionLength = args.messageText.trim().length;
11318
+ const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
11319
+ (block) => block.trim().length > 0
11320
+ ).length;
11321
+ const prompt = buildClassifierPrompt({
11322
+ conversationContext: trimmedContext,
11323
+ currentTurnBlocks: args.currentTurnBlocks,
11324
+ messageText: args.messageText
11325
+ });
11326
+ const logContext = {
11327
+ slackThreadId: args.context?.threadId,
11328
+ slackChannelId: args.context?.channelId,
11329
+ slackUserId: args.context?.requesterId,
11330
+ runId: args.context?.runId,
11331
+ modelId: args.fastModelId
11332
+ };
11333
+ return withSpan(
11334
+ "chat.route_thinking",
11335
+ "chat.route_thinking",
11336
+ logContext,
11337
+ async () => {
11338
+ setSpanAttributes({
11339
+ "app.ai.router.prompt_char_count": prompt.length,
11340
+ "app.ai.router.instruction_char_count": instructionLength,
11341
+ "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
11342
+ "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
11343
+ "app.ai.router.turn_block_count": turnBlockCount
11344
+ });
11345
+ const selection = await classifyTurn({
11346
+ completeObject: args.completeObject,
11347
+ fastModelId: args.fastModelId,
11348
+ metadata: {
11349
+ modelId: args.fastModelId,
11350
+ threadId: args.context?.threadId ?? "",
11351
+ channelId: args.context?.channelId ?? "",
11352
+ requesterId: args.context?.requesterId ?? "",
11353
+ runId: args.context?.runId ?? ""
11354
+ },
11355
+ prompt
11356
+ });
11357
+ const normalizedSelection = applyThinkingFloor(selection, {
11358
+ minimum: trimmedContext || turnBlockCount > 0 ? "medium" : void 0
11359
+ });
11360
+ setSpanAttributes({
11361
+ "app.ai.thinking_level": normalizedSelection.thinkingLevel,
11362
+ "app.ai.thinking_level_reason": normalizedSelection.reason,
11363
+ ...normalizedSelection.confidence !== void 0 ? {
11364
+ "app.ai.thinking_level_confidence": normalizedSelection.confidence
11365
+ } : {}
11287
11366
  });
11367
+ return normalizedSelection;
11288
11368
  }
11289
- return await upsertAgentTurnSessionRecord({
11290
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11291
- conversationId: args.conversationId,
11292
- cumulativeDurationMs,
11293
- cumulativeUsage,
11294
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11295
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11296
- sessionId: args.sessionId,
11297
- sliceId: nextSliceId,
11298
- state: "awaiting_resume",
11299
- piMessages,
11300
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11301
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11302
- resumeReason: "timeout",
11303
- resumedFromSliceId: args.currentSliceId,
11304
- errorMessage: args.errorMessage,
11305
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11306
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11307
- });
11308
- } catch (recordError) {
11309
- logSessionRecordError(
11310
- recordError,
11311
- "agent_continue_session_record_failed",
11312
- args,
11313
- {
11314
- "app.ai.resume_from_slice_id": args.currentSliceId,
11315
- "app.ai.resume_next_slice_id": nextSliceId
11316
- },
11317
- "Failed to persist session record before scheduling agent continuation"
11318
- );
11319
- return void 0;
11369
+ );
11370
+ }
11371
+ function applyThinkingFloor(selection, args) {
11372
+ const minimum = args.minimum;
11373
+ if (!minimum || selection.thinkingLevel === "none" || THINKING_LEVEL_RANK[selection.thinkingLevel] >= THINKING_LEVEL_RANK[minimum]) {
11374
+ return selection;
11320
11375
  }
11376
+ return {
11377
+ ...selection,
11378
+ thinkingLevel: minimum,
11379
+ reason: `thinking_floor:${minimum}:${selection.reason}`
11380
+ };
11321
11381
  }
11322
- async function persistYieldSessionRecord(args) {
11382
+ async function classifyTurn(args) {
11323
11383
  try {
11324
- const latestSessionRecord = await getAgentTurnSessionRecord(
11325
- args.conversationId,
11326
- args.sessionId
11327
- );
11328
- const piMessages = resumableBoundary(
11329
- args.messages,
11330
- latestSessionRecord?.piMessages
11331
- );
11332
- if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11333
- return void 0;
11334
- }
11335
- return await upsertAgentTurnSessionRecord({
11336
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11337
- conversationId: args.conversationId,
11338
- cumulativeDurationMs: addDurationMs(
11339
- latestSessionRecord?.cumulativeDurationMs,
11340
- args.currentDurationMs
11341
- ),
11342
- cumulativeUsage: addAgentTurnUsage(
11343
- latestSessionRecord?.cumulativeUsage,
11344
- args.currentUsage
11345
- ),
11346
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11347
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11348
- sessionId: args.sessionId,
11349
- sliceId: args.currentSliceId,
11350
- state: "awaiting_resume",
11351
- piMessages,
11352
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11353
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11354
- resumeReason: "yield",
11355
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
11356
- errorMessage: args.errorMessage,
11357
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11358
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11384
+ const result = await args.completeObject({
11385
+ modelId: args.fastModelId,
11386
+ schema: turnExecutionProfileSchema,
11387
+ maxTokens: 120,
11388
+ metadata: args.metadata,
11389
+ prompt: args.prompt,
11390
+ thinkingLevel: "low",
11391
+ system: buildClassifierSystemPrompt(),
11392
+ temperature: 0
11359
11393
  });
11360
- } catch (recordError) {
11361
- logSessionRecordError(
11362
- recordError,
11363
- "agent_turn_yield_session_record_failed",
11364
- args,
11365
- {
11366
- "app.ai.resume_slice_id": args.currentSliceId
11367
- },
11368
- "Failed to persist cooperative yield session record"
11369
- );
11370
- return void 0;
11394
+ const parsed = turnExecutionProfileSchema.parse(result.object);
11395
+ const reason = parsed.reason.trim();
11396
+ if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
11397
+ return {
11398
+ confidence: parsed.confidence,
11399
+ thinkingLevel: DEFAULT_THINKING_LEVEL,
11400
+ reason: `low_confidence_medium_default:${reason}`
11401
+ };
11402
+ }
11403
+ return {
11404
+ confidence: parsed.confidence,
11405
+ thinkingLevel: parsed.thinking_level,
11406
+ reason
11407
+ };
11408
+ } catch {
11409
+ return {
11410
+ thinkingLevel: DEFAULT_THINKING_LEVEL,
11411
+ reason: "classifier_error_default"
11412
+ };
11413
+ }
11414
+ }
11415
+ function toAgentThinkingLevel(level) {
11416
+ switch (level) {
11417
+ case "none":
11418
+ return "off";
11419
+ case "low":
11420
+ return "low";
11421
+ case "medium":
11422
+ return "medium";
11423
+ case "high":
11424
+ return "high";
11425
+ case "xhigh":
11426
+ return "xhigh";
11371
11427
  }
11372
11428
  }
11373
11429
 
@@ -12824,6 +12880,10 @@ function waitForAbortSettlement(promise, timeoutMs) {
12824
12880
  }
12825
12881
  var startupDiscoveryLogged = false;
12826
12882
  var MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS = 2e3;
12883
+ var legacyStoredTextPartSchema = z4.object({
12884
+ text: z4.string(),
12885
+ type: z4.literal("text")
12886
+ }).strict();
12827
12887
  function buildOmittedImageAttachmentNotice(count) {
12828
12888
  return [
12829
12889
  "<omitted-image-attachments>",
@@ -12988,7 +13048,7 @@ function buildUserTurnInput(args) {
12988
13048
  }
12989
13049
  function buildSteeringPiMessage(message) {
12990
13050
  const { userContentParts } = buildUserTurnInput({
12991
- userTurnText: message.text,
13051
+ userTurnText: buildUserTurnText(message.text),
12992
13052
  userAttachments: message.userAttachments,
12993
13053
  omittedImageAttachmentCount: message.omittedImageAttachmentCount ?? 0
12994
13054
  });
@@ -13006,15 +13066,52 @@ function withoutTrailingUncheckpointedUserPrompt(messages, userContentParts) {
13006
13066
  if (lastMessage?.role !== "user") {
13007
13067
  return messages;
13008
13068
  }
13009
- if (JSON.stringify(lastMessage.content) !== JSON.stringify(userContentParts)) {
13069
+ const comparableLastMessage = stripRuntimeTurnContext([
13070
+ lastMessage
13071
+ ])[0];
13072
+ if (!userPromptContentMatches(comparableLastMessage?.content, userContentParts)) {
13010
13073
  return messages;
13011
13074
  }
13012
13075
  return messages.slice(0, -1);
13013
13076
  }
13077
+ function userPromptContentMatches(storedContent, currentContent) {
13078
+ if (JSON.stringify(storedContent) === JSON.stringify(currentContent)) {
13079
+ return true;
13080
+ }
13081
+ if (!Array.isArray(storedContent)) {
13082
+ return false;
13083
+ }
13084
+ if (storedContent.length !== currentContent.length) {
13085
+ return false;
13086
+ }
13087
+ return storedContent.every((storedPart, index) => {
13088
+ const currentPart = currentContent[index];
13089
+ if (index === 0 && currentPart?.type === "text") {
13090
+ const legacyTextPart = legacyStoredTextPartSchema.safeParse(storedPart);
13091
+ if (legacyTextPart.success) {
13092
+ return legacyTextPartMatchesCurrentText(
13093
+ legacyTextPart.data.text,
13094
+ currentPart.text
13095
+ );
13096
+ }
13097
+ }
13098
+ return JSON.stringify(storedPart) === JSON.stringify(currentPart);
13099
+ });
13100
+ }
13101
+ function legacyTextPartMatchesCurrentText(storedText, currentText) {
13102
+ const storedInstructionBody = extractCurrentInstructionBody(storedText);
13103
+ if (storedInstructionBody !== void 0) {
13104
+ return renderCurrentInstruction(storedInstructionBody) === currentText;
13105
+ }
13106
+ return renderCurrentInstruction(storedText) === currentText;
13107
+ }
13014
13108
  async function generateAssistantReply(messageText2, context) {
13015
13109
  const conversationPrivacy = resolveConversationPrivacy({
13016
13110
  channelId: context.correlation?.channelId,
13017
- conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId
13111
+ conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
13112
+ // Source-confirmed visibility from the live event's channel_type; without
13113
+ // it the turn fails closed to private telemetry capture.
13114
+ visibility: context.slackConversation?.visibility
13018
13115
  });
13019
13116
  return runWithConversationPrivacy(
13020
13117
  conversationPrivacy ?? "private",
@@ -13177,14 +13274,26 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13177
13274
  if (!turnSessionState.canUseTurnSession || !sessionConversationId || !sessionId) {
13178
13275
  return;
13179
13276
  }
13180
- await recordToolExecutionStarted({
13181
- conversationId: sessionConversationId,
13182
- sessionId,
13183
- toolCallId: event.toolCallId,
13184
- toolName: event.toolName,
13185
- args: event.args,
13186
- ttlMs: THREAD_STATE_TTL_MS4
13187
- });
13277
+ try {
13278
+ await recordToolExecutionStarted({
13279
+ conversationId: sessionConversationId,
13280
+ sessionId,
13281
+ toolCallId: event.toolCallId,
13282
+ toolName: event.toolName,
13283
+ args: event.args,
13284
+ ttlMs: THREAD_STATE_TTL_MS4
13285
+ });
13286
+ } catch (error) {
13287
+ logException(
13288
+ error,
13289
+ "agent_turn_session_log_append_failed",
13290
+ spanContext,
13291
+ {
13292
+ "gen_ai.tool.name": event.toolName
13293
+ },
13294
+ "Failed to record host-only tool execution start"
13295
+ );
13296
+ }
13188
13297
  };
13189
13298
  const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
13190
13299
  configurationValues = {
@@ -13544,10 +13653,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13544
13653
  );
13545
13654
  const hasPromptCheckpoint = resumedFromSessionRecord && existingSessionRecord?.turnStartMessageIndex !== void 0;
13546
13655
  const shouldPromptAgent = !resumedFromSessionRecord || !hasPromptCheckpoint;
13547
- const promptHistoryMessages = shouldPromptAgent && resumedFromSessionRecord ? withoutTrailingUncheckpointedUserPrompt(
13656
+ const promptHistoryMessages = shouldPromptAgent ? withoutTrailingUncheckpointedUserPrompt(
13548
13657
  priorPiMessages,
13549
13658
  userContentParts
13550
- ) : shouldPromptAgent ? priorPiMessages ?? [] : existingSessionRecord.piMessages;
13659
+ ) : existingSessionRecord.piMessages;
13551
13660
  const needsBootstrapContextForPrompt = shouldPromptAgent && !hasRuntimeTurnContext(promptHistoryMessages);
13552
13661
  const systemPromptContributions = await getPluginSystemPromptContributions(toolSource);
13553
13662
  const pluginSystemPrompt = buildPluginSystemPromptContributions(
@@ -13702,12 +13811,12 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13702
13811
  });
13703
13812
  if (steeredMessageCount > 0) {
13704
13813
  logInfo(
13705
- "agent_turn_steering_messages_injected",
13814
+ "agent_turn_steering_messages_accepted",
13706
13815
  spanContext,
13707
13816
  {
13708
13817
  "app.ai.steering_message_count": steeredMessageCount
13709
13818
  },
13710
- "Agent turn steering messages injected"
13819
+ "Agent turn steering messages accepted"
13711
13820
  );
13712
13821
  }
13713
13822
  } catch (error) {
@@ -13798,10 +13907,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13798
13907
  beforeMessageCount = agent.state.messages.length;
13799
13908
  try {
13800
13909
  if (resumedFromSessionRecord) {
13801
- agent.state.messages = shouldPromptAgent ? promptHistoryMessages ?? [] : existingSessionRecord.piMessages;
13910
+ agent.state.messages = shouldPromptAgent ? promptHistoryMessages : existingSessionRecord.piMessages;
13802
13911
  turnStartMessageIndex = existingSessionRecord.turnStartMessageIndex;
13803
- } else if (context.piMessages && context.piMessages.length > 0) {
13804
- agent.state.messages = [...context.piMessages];
13912
+ } else if (promptHistoryMessages.length > 0) {
13913
+ agent.state.messages = [...promptHistoryMessages];
13805
13914
  }
13806
13915
  beforeMessageCount = agent.state.messages.length;
13807
13916
  if (shouldPromptAgent) {
@@ -13969,22 +14078,6 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13969
14078
  }
13970
14079
  if (turnSessionState.canUseTurnSession && sessionConversationId && sessionId) {
13971
14080
  await recordActiveMcpProviders();
13972
- await persistCompletedSessionRecord({
13973
- channelName: context.correlation?.channelName,
13974
- conversationId: sessionConversationId,
13975
- currentDurationMs: Date.now() - replyStartedAtMs,
13976
- currentUsage: turnUsage,
13977
- destination: context.destination,
13978
- source: runSource,
13979
- sessionId,
13980
- sliceId: currentSliceId,
13981
- allMessages: agent.state.messages,
13982
- loadedSkillNames: loadedSkillNamesForResume,
13983
- logContext: sessionRecordLogContext,
13984
- requester,
13985
- ...surface ? { surface } : {},
13986
- ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
13987
- });
13988
14081
  }
13989
14082
  return buildTurnResult({
13990
14083
  newMessages,
@@ -14146,7 +14239,7 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
14146
14239
  );
14147
14240
  const message = error instanceof Error ? error.message : String(error);
14148
14241
  return {
14149
- text: `Error: ${message}`,
14242
+ text: "",
14150
14243
  ...getSandboxMetadata(),
14151
14244
  diagnostics: {
14152
14245
  outcome: "provider_error",
@@ -14537,6 +14630,98 @@ function getConversationMessageSlackTs(message) {
14537
14630
  return message.meta?.slackTs ?? toOptionalString(message.id);
14538
14631
  }
14539
14632
 
14633
+ // src/chat/services/turn-failure-response.ts
14634
+ function requireTurnFailureEventId(eventId, eventName) {
14635
+ if (!eventId) {
14636
+ throw new Error(`Sentry did not return an event ID for ${eventName}`);
14637
+ }
14638
+ return eventId;
14639
+ }
14640
+ function getExecutionFailureReason(reply) {
14641
+ const errorMessage = reply.diagnostics.errorMessage?.trim();
14642
+ if (errorMessage) {
14643
+ return errorMessage;
14644
+ }
14645
+ if (reply.diagnostics.toolErrorCount > 0) {
14646
+ return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
14647
+ }
14648
+ if (reply.diagnostics.assistantMessageCount > 0) {
14649
+ return "assistant returned no text";
14650
+ }
14651
+ return "empty assistant turn";
14652
+ }
14653
+ function getFailureCapture(reply) {
14654
+ if (reply.diagnostics.outcome === "provider_error") {
14655
+ return {
14656
+ eventName: "agent_turn_provider_error",
14657
+ error: reply.diagnostics.providerError ?? new Error(
14658
+ reply.diagnostics.errorMessage ?? "Provider error without explicit message"
14659
+ ),
14660
+ attributes: {},
14661
+ body: "Agent turn failed with provider error"
14662
+ };
14663
+ }
14664
+ const failureReason = getExecutionFailureReason(reply);
14665
+ return {
14666
+ eventName: "agent_turn_execution_failure",
14667
+ error: new Error(`Agent turn execution failure: ${failureReason}`),
14668
+ attributes: {
14669
+ "app.ai.execution_failure_reason": failureReason
14670
+ },
14671
+ body: "Agent turn completed with execution failure"
14672
+ };
14673
+ }
14674
+ function getAgentTurnDiagnosticsAttributes(reply) {
14675
+ return {
14676
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
14677
+ "gen_ai.operation.name": "invoke_agent",
14678
+ "app.ai.outcome": reply.diagnostics.outcome,
14679
+ "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
14680
+ "app.ai.tool_results": reply.diagnostics.toolResultCount,
14681
+ "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
14682
+ "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
14683
+ "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
14684
+ ...reply.diagnostics.thinkingLevel ? {
14685
+ "app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
14686
+ } : {},
14687
+ ...reply.diagnostics.stopReason ? {
14688
+ "gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
14689
+ } : {},
14690
+ ...reply.diagnostics.errorMessage ? { "exception.message": reply.diagnostics.errorMessage } : {}
14691
+ };
14692
+ }
14693
+ function finalizeFailedTurnReply(args) {
14694
+ if (args.reply.diagnostics.outcome === "success") {
14695
+ return args.reply;
14696
+ }
14697
+ const capture = getFailureCapture(args.reply);
14698
+ const eventId = requireTurnFailureEventId(
14699
+ args.logException(
14700
+ capture.error,
14701
+ capture.eventName,
14702
+ args.context,
14703
+ {
14704
+ ...getAgentTurnDiagnosticsAttributes(args.reply),
14705
+ ...args.attributes,
14706
+ ...capture.attributes
14707
+ },
14708
+ capture.body
14709
+ ),
14710
+ capture.eventName
14711
+ );
14712
+ const providerPartialText = args.reply.diagnostics.outcome === "provider_error" && args.reply.diagnostics.assistantMessageCount > 0 ? args.reply.text.trim() : "";
14713
+ return {
14714
+ ...args.reply,
14715
+ text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
14716
+ deliveryMode: "thread",
14717
+ deliveryPlan: {
14718
+ mode: "thread",
14719
+ postThreadText: true,
14720
+ attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
14721
+ }
14722
+ };
14723
+ }
14724
+
14540
14725
  // src/chat/plugins/task-runner.ts
14541
14726
  import { pluginRunContextSchema } from "@sentry/junior-plugin-api";
14542
14727
  var PLUGIN_TASK_LOCK_TTL_MS = 5 * 60 * 1e3;
@@ -14812,13 +14997,19 @@ export {
14812
14997
  isCooperativeTurnYieldError,
14813
14998
  TurnInputCommitLostError,
14814
14999
  isTurnInputCommitLostError,
15000
+ TurnInputDeferredError,
15001
+ isTurnInputDeferredError,
14815
15002
  startActiveTurn,
14816
15003
  markTurnClosed,
14817
15004
  markTurnFailed,
14818
15005
  isRedundantReactionAckText,
15006
+ persistCompletedSessionRecord,
15007
+ completeDeliveredTurn,
15008
+ persistYieldSessionRecord,
14819
15009
  finalizeMcpAuthorization,
14820
15010
  resolvePluginOAuthAccount,
14821
15011
  executeCredentialedEgressRequest,
15012
+ buildSteeringPiMessage,
14822
15013
  generateAssistantReply,
14823
15014
  estimateTextTokens,
14824
15015
  getAgentContextCompactionTriggerTokens,
@@ -14832,6 +15023,9 @@ export {
14832
15023
  createConversationMemoryService,
14833
15024
  isHumanConversationMessage,
14834
15025
  getConversationMessageSlackTs,
15026
+ requireTurnFailureEventId,
15027
+ getAgentTurnDiagnosticsAttributes,
15028
+ finalizeFailedTurnReply,
14835
15029
  scheduleSessionCompletedPluginTasks,
14836
15030
  processPluginTask,
14837
15031
  buildDeliveredTurnStatePatch