@sentry/junior 0.85.0 → 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 (47) hide show
  1. package/dist/{agent-hooks-AV7CHLUO.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +788 -333
  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/ingress/message-changed.d.ts +2 -0
  8. package/dist/chat/respond.d.ts +7 -1
  9. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  10. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  11. package/dist/chat/runtime/turn.d.ts +7 -0
  12. package/dist/chat/services/persist-retry.d.ts +2 -0
  13. package/dist/chat/services/turn-session-record.d.ts +31 -2
  14. package/dist/chat/slack/client.d.ts +33 -2
  15. package/dist/chat/slack/conversation-context.d.ts +15 -0
  16. package/dist/chat/state/turn-session.d.ts +9 -0
  17. package/dist/chat/task-execution/queue.d.ts +1 -1
  18. package/dist/chat/task-execution/slack-work.d.ts +10 -2
  19. package/dist/chat/task-execution/state.d.ts +44 -4
  20. package/dist/chat/task-execution/store.d.ts +21 -4
  21. package/dist/chat/task-execution/worker.d.ts +11 -4
  22. package/dist/chat/tools/slack/channel-access.d.ts +29 -0
  23. package/dist/chat/tools/slack/thread-read.d.ts +4 -1
  24. package/dist/{chunk-UVNV67EC.js → chunk-2NFV5FMB.js} +4 -4
  25. package/dist/{chunk-GY7L6VCT.js → chunk-37B2R2QJ.js} +15 -10
  26. package/dist/{chunk-QTTTRBNB.js → chunk-6I6HBOQM.js} +80 -15
  27. package/dist/{chunk-ENEWQMRJ.js → chunk-6O5UI3RG.js} +1 -1
  28. package/dist/{chunk-MTE5NRPJ.js → chunk-BRSQQRG6.js} +1 -1
  29. package/dist/{chunk-EENOFKDN.js → chunk-ENPSU7L7.js} +165 -27
  30. package/dist/{chunk-MHEV3T2Y.js → chunk-FPHA6GCQ.js} +876 -719
  31. package/dist/{chunk-RUB77TGS.js → chunk-GB5DFM4D.js} +1 -1
  32. package/dist/{chunk-C2PSUWK3.js → chunk-GGD6WK6V.js} +124 -25
  33. package/dist/{chunk-SSUEWAVS.js → chunk-JRXCSSSU.js} +19 -4
  34. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  35. package/dist/{chunk-FYPO4K7C.js → chunk-RIB3M6YA.js} +2 -2
  36. package/dist/{chunk-BU2AWPEN.js → chunk-ZU2ALUVQ.js} +3 -2
  37. package/dist/cli/chat.js +4 -4
  38. package/dist/cli/plugins.js +7 -7
  39. package/dist/cli/snapshot-warmup.js +4 -4
  40. package/dist/cli/upgrade.js +10 -7
  41. package/dist/{db-S3HYBLUS.js → db-SZVUU7RB.js} +4 -4
  42. package/dist/instrumentation.js +1 -1
  43. package/dist/nitro.js +4 -4
  44. package/dist/reporting/conversations.d.ts +2 -1
  45. package/dist/reporting.js +95 -30
  46. package/dist/{runner-IMQH6V2B.js → runner-HEBRPNN2.js} +27 -13
  47. package/package.json +6 -6
@@ -10,7 +10,7 @@ import {
10
10
  isSnapshotMissingError,
11
11
  resolveRuntimeDependencySnapshot,
12
12
  runNonInteractiveCommand
13
- } from "./chunk-FYPO4K7C.js";
13
+ } from "./chunk-RIB3M6YA.js";
14
14
  import {
15
15
  pluginTaskId,
16
16
  pluginTaskParamsSchema,
@@ -23,6 +23,7 @@ import {
23
23
  buildSystemPrompt,
24
24
  buildTurnContextPrompt,
25
25
  getAgentTurnSessionRecord,
26
+ getInterruptionMarker,
26
27
  loadConnectedMcpProviders,
27
28
  recordAuthorizationRequested,
28
29
  recordMcpProviderConnected,
@@ -30,7 +31,7 @@ import {
30
31
  recordSubagentStarted,
31
32
  recordToolExecutionStarted,
32
33
  upsertAgentTurnSessionRecord
33
- } from "./chunk-GY7L6VCT.js";
34
+ } from "./chunk-37B2R2QJ.js";
34
35
  import {
35
36
  createPluginEmbedder,
36
37
  createPluginHookRunner,
@@ -41,14 +42,15 @@ import {
41
42
  getPlugins,
42
43
  getSlackToolContext,
43
44
  resolveChannelCapabilities
44
- } from "./chunk-UVNV67EC.js";
45
+ } from "./chunk-2NFV5FMB.js";
45
46
  import {
46
47
  createPluginLogger,
47
48
  createPluginState
48
- } from "./chunk-MTE5NRPJ.js";
49
+ } from "./chunk-BRSQQRG6.js";
49
50
  import {
51
+ getConversationStore,
50
52
  getDb
51
- } from "./chunk-QTTTRBNB.js";
53
+ } from "./chunk-6I6HBOQM.js";
52
54
  import {
53
55
  SANDBOX_DATA_ROOT,
54
56
  SANDBOX_SKILLS_ROOT,
@@ -59,7 +61,7 @@ import {
59
61
  import {
60
62
  acquireActiveLock,
61
63
  getStateAdapter
62
- } from "./chunk-RUB77TGS.js";
64
+ } from "./chunk-GB5DFM4D.js";
63
65
  import {
64
66
  SlackActionError,
65
67
  downloadPrivateSlackFile,
@@ -70,7 +72,7 @@ import {
70
72
  normalizeSlackConversationId,
71
73
  parseDestination,
72
74
  withSlackRetries
73
- } from "./chunk-C2PSUWK3.js";
75
+ } from "./chunk-GGD6WK6V.js";
74
76
  import {
75
77
  GEN_AI_PROVIDER_NAME,
76
78
  GEN_AI_SERVER_ADDRESS,
@@ -106,7 +108,7 @@ import {
106
108
  toObservablePromptPart,
107
109
  trimTrailingAssistantMessages,
108
110
  upsertActiveSkill
109
- } from "./chunk-BU2AWPEN.js";
111
+ } from "./chunk-ZU2ALUVQ.js";
110
112
  import {
111
113
  parseSlackThreadId,
112
114
  resolveConversationPrivacy,
@@ -116,7 +118,7 @@ import {
116
118
  toGenAiPayloadMetadata,
117
119
  toGenAiPayloadTraceAttributes,
118
120
  toGenAiTextMetadata
119
- } from "./chunk-DR75T7J3.js";
121
+ } from "./chunk-L7OHKDOX.js";
120
122
  import {
121
123
  discoverSkills,
122
124
  findSkillByName,
@@ -138,6 +140,7 @@ import {
138
140
  listReferenceFiles
139
141
  } from "./chunk-6APU57E6.js";
140
142
  import {
143
+ buildTurnFailureResponse,
141
144
  extractGenAiUsageAttributes,
142
145
  extractGenAiUsageSummary,
143
146
  getActiveTraceId,
@@ -650,6 +653,16 @@ var TurnInputCommitLostError = class extends Error {
650
653
  function isTurnInputCommitLostError(error) {
651
654
  return error instanceof TurnInputCommitLostError;
652
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
+ }
653
666
  function startActiveTurn(args) {
654
667
  args.conversation.processing.activeTurnId = args.nextTurnId;
655
668
  args.updateConversationStats(args.conversation);
@@ -679,186 +692,593 @@ function markTurnFailed(args) {
679
692
  args.updateConversationStats(args.conversation);
680
693
  }
681
694
 
682
- // src/chat/respond.ts
683
- import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
684
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
685
- import { z as z4 } from "zod";
686
-
687
- // src/chat/capabilities/catalog.ts
688
- var cachedCatalog;
689
- function getCapabilityCatalog() {
690
- const signature = pluginCatalogRuntime.getSignature();
691
- if (cachedCatalog?.signature === signature) return cachedCatalog;
692
- const providers = pluginCatalogRuntime.getCapabilityProviders();
693
- const capabilityToProvider = /* @__PURE__ */ new Map();
694
- for (const provider of providers) {
695
- for (const capability of provider.capabilities) {
696
- if (capabilityToProvider.has(capability)) {
697
- throw new Error(
698
- `Duplicate capability registration for "${capability}"`
699
- );
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;
700
734
  }
701
- capabilityToProvider.set(capability, provider);
735
+ continue;
736
+ }
737
+ const totalTokens = getFiniteCount(usage.totalTokens);
738
+ if (totalTokens !== void 0) {
739
+ totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
702
740
  }
703
741
  }
704
- cachedCatalog = { signature, providers, capabilityToProvider };
705
- return cachedCatalog;
742
+ if (totalOnlyTokens !== void 0) {
743
+ return {
744
+ totalTokens: totalOnlyTokens + (componentTotal ?? 0)
745
+ };
746
+ }
747
+ return hasAgentTurnUsage(components) ? components : void 0;
706
748
  }
707
- var catalogLogged = false;
708
- function logCapabilityCatalogLoadedOnce() {
709
- if (catalogLogged) return;
710
- catalogLogged = true;
711
- const { providers } = getCapabilityCatalog();
712
- const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
713
- const configKeys = [
714
- ...new Set(providers.flatMap((p) => p.configKeys))
715
- ].sort();
716
- logInfo(
717
- "capability_catalog_loaded",
718
- {},
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,
719
756
  {
720
- "app.capability.providers": providers.map((p) => p.provider),
721
- "app.capability.count": capabilityNames.length,
722
- "app.capability.names": capabilityNames,
723
- "app.config.key_count": configKeys.length,
724
- "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
725
763
  },
726
- "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
727
770
  );
728
771
  }
729
-
730
- // src/chat/capabilities/router.ts
731
- var ProviderCredentialRouter = class {
732
- brokersByProvider;
733
- constructor(input) {
734
- 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;
735
789
  }
736
- async issue(input) {
737
- const broker = this.brokersByProvider[input.provider];
738
- 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) {
739
856
  throw new Error(
740
- `No credential broker registered for provider: ${input.provider}`
857
+ "Completed session record requires a slice id from the caller or the latest stored record"
741
858
  );
742
859
  }
743
- return await broker.issue({
744
- context: input.context,
745
- 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
+ } : {}
746
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
+ );
747
896
  }
748
- };
749
-
750
- // src/chat/credentials/user-token-store.ts
751
- import {
752
- pluginStoredTokensSchema
753
- } from "@sentry/junior-plugin-api";
754
- var storedTokensSchema = pluginStoredTokensSchema;
755
-
756
- // src/chat/credentials/state-adapter-token-store.ts
757
- var KEY_PREFIX = "oauth-token";
758
- var BUFFER_MS = 24 * 60 * 60 * 1e3;
759
- var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
760
- var REFRESH_LOCK_WAIT_MS = 3e4;
761
- var REFRESH_LOCK_RETRY_MS = 100;
762
- function tokenKey(userId, provider) {
763
- return `${KEY_PREFIX}:${userId}:${provider}`;
764
897
  }
765
- function refreshLockKey(userId, provider) {
766
- 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
+ });
767
916
  }
768
- async function sleep(ms) {
769
- await new Promise((resolve) => setTimeout(resolve, ms));
770
- }
771
- var StateAdapterTokenStore = class {
772
- state;
773
- constructor(stateAdapter) {
774
- this.state = stateAdapter;
775
- }
776
- async get(userId, provider) {
777
- const stored = await this.state.get(tokenKey(userId, provider));
778
- return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
779
- }
780
- async set(userId, provider, tokens) {
781
- const parsed = storedTokensSchema.parse(tokens);
782
- const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
783
- const ttlMs2 = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
784
- await this.state.set(tokenKey(userId, provider), parsed, ttlMs2);
785
- }
786
- async delete(userId, provider) {
787
- await this.state.delete(tokenKey(userId, provider));
788
- }
789
- /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
790
- async withRefresh(userId, provider, callback) {
791
- const lockKey = refreshLockKey(userId, provider);
792
- const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
793
- while (true) {
794
- const lock = await acquireActiveLock(this.state, lockKey);
795
- if (lock) {
796
- try {
797
- return await callback();
798
- } finally {
799
- await this.state.releaseLock(lock);
800
- }
801
- }
802
- if (Date.now() >= deadline) {
803
- throw new Error(`Could not acquire OAuth token refresh lock`);
804
- }
805
- 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;
806
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
+ );
807
967
  }
808
- };
809
-
810
- // src/chat/capabilities/factory.ts
811
- var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
812
- function createUserTokenStore() {
813
- return new StateAdapterTokenStore(getStateAdapter());
968
+ return void 0;
814
969
  }
815
- function createProviderCredentialRouter(userTokenStore) {
816
- logCapabilityCatalogLoadedOnce();
817
- const brokersByProvider = {};
818
- for (const plugin of pluginCatalogRuntime.getProviders()) {
819
- const { name } = plugin.manifest;
820
- if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
821
- 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;
822
983
  }
823
- brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
824
- 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 } : {}
825
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;
826
1048
  }
827
- return new ProviderCredentialRouter({ brokersByProvider });
828
1049
  }
829
- function getSandboxEgressRouter() {
830
- const stateAdapter = getStateAdapter();
831
- let router = sandboxEgressRouters.get(stateAdapter);
832
- if (!router) {
833
- router = createProviderCredentialRouter(
834
- new StateAdapterTokenStore(stateAdapter)
1050
+ async function persistYieldSessionRecord(args) {
1051
+ try {
1052
+ const latestSessionRecord = await getAgentTurnSessionRecord(
1053
+ args.conversationId,
1054
+ args.sessionId
835
1055
  );
836
- 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;
837
1099
  }
838
- return router;
839
- }
840
- async function issueProviderCredentialLease(input) {
841
- return await getSandboxEgressRouter().issue(input);
842
1100
  }
843
1101
 
844
- // src/chat/capabilities/jr-rpc-command.ts
845
- import { Bash, defineCommand } from "just-bash";
846
- function commandResult(input) {
847
- let stdout = "";
848
- if (typeof input.stdout === "string") {
849
- stdout = input.stdout;
850
- } else if (input.stdout !== void 0) {
851
- stdout = `${JSON.stringify(input.stdout, null, 2)}
852
- `;
853
- }
854
- return {
855
- stdout,
856
- stderr: input.stderr ?? "",
857
- exitCode: input.exitCode
858
- };
859
- }
860
- function requireChannelConfiguration(deps) {
861
- if (deps.channelConfiguration) {
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) {
1281
+ if (deps.channelConfiguration) {
862
1282
  return { ok: true, configuration: deps.channelConfiguration };
863
1283
  }
864
1284
  return {
@@ -4470,6 +4890,7 @@ async function getPermalinkBestEffort(args) {
4470
4890
  3,
4471
4891
  {
4472
4892
  action: "chat.getPermalink",
4893
+ idempotent: true,
4473
4894
  spanAttributes: {
4474
4895
  "app.slack.channel_id": args.channelId,
4475
4896
  "app.slack.message_ts": args.messageTs
@@ -4539,6 +4960,7 @@ async function deleteSlackMessage(input) {
4539
4960
  3,
4540
4961
  {
4541
4962
  action: "chat.delete",
4963
+ idempotent: true,
4542
4964
  spanAttributes: {
4543
4965
  "app.slack.channel_id": channelId,
4544
4966
  "app.slack.message_ts": timestamp
@@ -4647,6 +5069,7 @@ async function addReactionToMessage(input) {
4647
5069
  3,
4648
5070
  {
4649
5071
  action: "reactions.add",
5072
+ idempotent: true,
4650
5073
  spanAttributes: {
4651
5074
  "app.slack.channel_id": channelId,
4652
5075
  "app.slack.message_ts": timestamp,
@@ -4685,6 +5108,7 @@ async function removeReactionFromMessage(input) {
4685
5108
  3,
4686
5109
  {
4687
5110
  action: "reactions.remove",
5111
+ idempotent: true,
4688
5112
  spanAttributes: {
4689
5113
  "app.slack.channel_id": channelId,
4690
5114
  "app.slack.message_ts": timestamp,
@@ -5750,6 +6174,31 @@ function createSlackListUpdateItemTool(state) {
5750
6174
  // src/chat/tools/slack/thread-read.ts
5751
6175
  import { Type as Type19 } from "@sinclair/typebox";
5752
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
+
5753
6202
  // src/chat/tools/slack/slack-message-url.ts
5754
6203
  var SLACK_HOST_PATTERN = /^[a-z0-9-]+\.slack(?:-gov)?\.com$/;
5755
6204
  var ARCHIVE_PATH_PATTERN = /^\/archives\/([CDG][A-Z0-9]+)\/p(\d{10})(\d{6})$/;
@@ -5909,26 +6358,9 @@ function truncateMessages(messages, maxChars) {
5909
6358
  }
5910
6359
  return { messages: kept, omitted: messages.length - kept.length };
5911
6360
  }
5912
- function checkChannelAccess(targetChannelId, currentChannelId) {
5913
- const target = normalizeSlackConversationId(targetChannelId);
5914
- const current = normalizeSlackConversationId(currentChannelId);
5915
- if (!target) {
5916
- return { allowed: false, error: "Invalid Slack channel ID." };
5917
- }
5918
- if (target.startsWith("C")) {
5919
- return { allowed: true };
5920
- }
5921
- if (target === current) {
5922
- return { allowed: true };
5923
- }
5924
- return {
5925
- allowed: false,
5926
- error: "Cannot read private channels or DMs unless the link is from the current conversation."
5927
- };
5928
- }
5929
- function createSlackThreadReadTool(context) {
6361
+ function createSlackThreadReadTool(context, deps = {}) {
5930
6362
  return tool({
5931
- 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.",
5932
6364
  annotations: { readOnlyHint: true, destructiveHint: false },
5933
6365
  inputSchema: Type19.Object({
5934
6366
  url: Type19.Optional(
@@ -5988,10 +6420,15 @@ function createSlackThreadReadTool(context) {
5988
6420
  error: "Provide either a Slack message `url` or both `channel_id` and `ts`."
5989
6421
  };
5990
6422
  }
5991
- const access = checkChannelAccess(
5992
- channelId,
5993
- context.destinationChannelId
5994
- );
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
+ });
5995
6432
  if (!access.allowed) {
5996
6433
  return {
5997
6434
  ok: false,
@@ -10860,513 +11297,133 @@ function buildClassifierPrompt(args) {
10860
11297
  sections.push(
10861
11298
  "<thread-background>",
10862
11299
  contextText,
10863
- "</thread-background>",
10864
- ""
10865
- );
10866
- }
10867
- }
10868
- sections.push(renderCurrentInstruction(args.messageText.trim() || "[empty]"));
10869
- for (const block of args.currentTurnBlocks ?? []) {
10870
- const trimmed = block.trim();
10871
- if (!trimmed) {
10872
- continue;
10873
- }
10874
- sections.push("", trimmed);
10875
- }
10876
- return sections.join("\n");
10877
- }
10878
- async function selectTurnThinkingLevel(args) {
10879
- const trimmedContext = trimContextForRouter(args.conversationContext);
10880
- const instructionLength = args.messageText.trim().length;
10881
- const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
10882
- (block) => block.trim().length > 0
10883
- ).length;
10884
- const prompt = buildClassifierPrompt({
10885
- conversationContext: trimmedContext,
10886
- currentTurnBlocks: args.currentTurnBlocks,
10887
- messageText: args.messageText
10888
- });
10889
- const logContext = {
10890
- slackThreadId: args.context?.threadId,
10891
- slackChannelId: args.context?.channelId,
10892
- slackUserId: args.context?.requesterId,
10893
- runId: args.context?.runId,
10894
- modelId: args.fastModelId
10895
- };
10896
- return withSpan(
10897
- "chat.route_thinking",
10898
- "chat.route_thinking",
10899
- logContext,
10900
- async () => {
10901
- setSpanAttributes({
10902
- "app.ai.router.prompt_char_count": prompt.length,
10903
- "app.ai.router.instruction_char_count": instructionLength,
10904
- "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
10905
- "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
10906
- "app.ai.router.turn_block_count": turnBlockCount
10907
- });
10908
- const selection = await classifyTurn({
10909
- completeObject: args.completeObject,
10910
- fastModelId: args.fastModelId,
10911
- metadata: {
10912
- modelId: args.fastModelId,
10913
- threadId: args.context?.threadId ?? "",
10914
- channelId: args.context?.channelId ?? "",
10915
- requesterId: args.context?.requesterId ?? "",
10916
- runId: args.context?.runId ?? ""
10917
- },
10918
- prompt
10919
- });
10920
- const normalizedSelection = applyThinkingFloor(selection, {
10921
- minimum: trimmedContext || turnBlockCount > 0 ? "medium" : void 0
10922
- });
10923
- setSpanAttributes({
10924
- "app.ai.thinking_level": normalizedSelection.thinkingLevel,
10925
- "app.ai.thinking_level_reason": normalizedSelection.reason,
10926
- ...normalizedSelection.confidence !== void 0 ? {
10927
- "app.ai.thinking_level_confidence": normalizedSelection.confidence
10928
- } : {}
10929
- });
10930
- return normalizedSelection;
10931
- }
10932
- );
10933
- }
10934
- function applyThinkingFloor(selection, args) {
10935
- const minimum = args.minimum;
10936
- if (!minimum || selection.thinkingLevel === "none" || THINKING_LEVEL_RANK[selection.thinkingLevel] >= THINKING_LEVEL_RANK[minimum]) {
10937
- return selection;
10938
- }
10939
- return {
10940
- ...selection,
10941
- thinkingLevel: minimum,
10942
- reason: `thinking_floor:${minimum}:${selection.reason}`
10943
- };
10944
- }
10945
- async function classifyTurn(args) {
10946
- try {
10947
- const result = await args.completeObject({
10948
- modelId: args.fastModelId,
10949
- schema: turnExecutionProfileSchema,
10950
- maxTokens: 120,
10951
- metadata: args.metadata,
10952
- prompt: args.prompt,
10953
- thinkingLevel: "low",
10954
- system: buildClassifierSystemPrompt(),
10955
- temperature: 0
10956
- });
10957
- const parsed = turnExecutionProfileSchema.parse(result.object);
10958
- const reason = parsed.reason.trim();
10959
- if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
10960
- return {
10961
- confidence: parsed.confidence,
10962
- thinkingLevel: DEFAULT_THINKING_LEVEL,
10963
- reason: `low_confidence_medium_default:${reason}`
10964
- };
10965
- }
10966
- return {
10967
- confidence: parsed.confidence,
10968
- thinkingLevel: parsed.thinking_level,
10969
- reason
10970
- };
10971
- } catch {
10972
- return {
10973
- thinkingLevel: DEFAULT_THINKING_LEVEL,
10974
- reason: "classifier_error_default"
10975
- };
10976
- }
10977
- }
10978
- function toAgentThinkingLevel(level) {
10979
- switch (level) {
10980
- case "none":
10981
- return "off";
10982
- case "low":
10983
- return "low";
10984
- case "medium":
10985
- return "medium";
10986
- case "high":
10987
- return "high";
10988
- case "xhigh":
10989
- return "xhigh";
10990
- }
10991
- }
10992
-
10993
- // src/chat/usage.ts
10994
- var COMPONENT_USAGE_FIELDS = [
10995
- "inputTokens",
10996
- "outputTokens",
10997
- "cachedInputTokens",
10998
- "cacheCreationTokens"
10999
- ];
11000
- function hasAgentTurnUsage(usage) {
11001
- return Boolean(
11002
- usage && Object.values(usage).some(
11003
- (value) => typeof value === "number" && Number.isFinite(value)
11004
- )
11005
- );
11006
- }
11007
- function getFiniteCount(value) {
11008
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
11009
- }
11010
- function getComponentTotal(usage) {
11011
- let total;
11012
- for (const field of COMPONENT_USAGE_FIELDS) {
11013
- const value = getFiniteCount(usage[field]);
11014
- if (value === void 0) continue;
11015
- total = (total ?? 0) + value;
11016
- }
11017
- return total;
11018
- }
11019
- function addAgentTurnUsage(...usages) {
11020
- const components = {};
11021
- let componentTotal;
11022
- let totalOnlyTokens;
11023
- for (const usage of usages) {
11024
- if (!usage) continue;
11025
- const usageComponentTotal = getComponentTotal(usage);
11026
- if (usageComponentTotal !== void 0) {
11027
- componentTotal = (componentTotal ?? 0) + usageComponentTotal;
11028
- for (const field of COMPONENT_USAGE_FIELDS) {
11029
- const value = getFiniteCount(usage[field]);
11030
- if (value === void 0) continue;
11031
- components[field] = (components[field] ?? 0) + value;
11032
- }
11033
- continue;
11034
- }
11035
- const totalTokens = getFiniteCount(usage.totalTokens);
11036
- if (totalTokens !== void 0) {
11037
- totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
11038
- }
11039
- }
11040
- if (totalOnlyTokens !== void 0) {
11041
- return {
11042
- totalTokens: totalOnlyTokens + (componentTotal ?? 0)
11043
- };
11044
- }
11045
- return hasAgentTurnUsage(components) ? components : void 0;
11046
- }
11047
-
11048
- // src/chat/services/turn-session-record.ts
11049
- var AGENT_CONTINUE_MAX_SLICES = 48;
11050
- function logSessionRecordError(error, eventName, args, attributes, message) {
11051
- logException(
11052
- error,
11053
- eventName,
11054
- {
11055
- slackThreadId: args.logContext.threadId,
11056
- slackUserId: args.logContext.requesterId,
11057
- slackChannelId: args.logContext.channelId,
11058
- runId: args.logContext.runId,
11059
- assistantUserName: args.logContext.assistantUserName,
11060
- modelId: args.logContext.modelId
11061
- },
11062
- {
11063
- "app.ai.resume_conversation_id": args.conversationId,
11064
- "app.ai.resume_session_id": args.sessionId,
11065
- ...attributes
11066
- },
11067
- message
11068
- );
11069
- }
11070
- function addDurationMs(prior, current) {
11071
- const total = [prior, current].reduce((sum, value) => {
11072
- if (typeof value !== "number" || !Number.isFinite(value)) {
11073
- return sum;
11074
- }
11075
- return (sum ?? 0) + Math.max(0, Math.floor(value));
11076
- }, void 0);
11077
- return total;
11078
- }
11079
- function isContinuableBoundary(messages) {
11080
- const lastRole = getPiMessageRole(messages.at(-1));
11081
- return lastRole === "user" || lastRole === "toolResult";
11082
- }
11083
- function resumableBoundary(messages, fallbackMessages) {
11084
- const current = trimTrailingAssistantMessages(messages);
11085
- if (current.length > 0 && isContinuableBoundary(current)) {
11086
- return current;
11087
- }
11088
- return trimTrailingAssistantMessages(fallbackMessages ?? []);
11089
- }
11090
- async function loadTurnSessionRecord(ctx) {
11091
- const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
11092
- const existingSessionRecord = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionRecord(ctx.conversationId, ctx.sessionId) : void 0;
11093
- const hasAwaitingResumeRecord = Boolean(
11094
- existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
11095
- );
11096
- return {
11097
- canUseTurnSession,
11098
- resumedFromSessionRecord: hasAwaitingResumeRecord,
11099
- currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
11100
- existingSessionRecord
11101
- };
11102
- }
11103
- async function persistRunningSessionRecord(args) {
11104
- if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
11105
- return false;
11106
- }
11107
- try {
11108
- const latestSessionRecord = await getAgentTurnSessionRecord(
11109
- args.conversationId,
11110
- args.sessionId
11111
- );
11112
- await upsertAgentTurnSessionRecord({
11113
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11114
- conversationId: args.conversationId,
11115
- cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
11116
- cumulativeUsage: latestSessionRecord?.cumulativeUsage,
11117
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11118
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11119
- sessionId: args.sessionId,
11120
- sliceId: args.sliceId,
11121
- state: "running",
11122
- piMessages: args.messages,
11123
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11124
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11125
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11126
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
11127
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
11128
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
11129
- } : {}
11130
- });
11131
- return true;
11132
- } catch (recordError) {
11133
- logSessionRecordError(
11134
- recordError,
11135
- "agent_turn_running_session_record_failed",
11136
- args,
11137
- {
11138
- "app.ai.resume_slice_id": args.sliceId
11139
- },
11140
- "Failed to persist running turn session record"
11141
- );
11142
- return false;
11143
- }
11144
- }
11145
- async function persistCompletedSessionRecord(args) {
11146
- try {
11147
- const latestSessionRecord = await getAgentTurnSessionRecord(
11148
- args.conversationId,
11149
- args.sessionId
11150
- );
11151
- await upsertAgentTurnSessionRecord({
11152
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11153
- conversationId: args.conversationId,
11154
- cumulativeDurationMs: addDurationMs(
11155
- latestSessionRecord?.cumulativeDurationMs,
11156
- args.currentDurationMs
11157
- ),
11158
- cumulativeUsage: addAgentTurnUsage(
11159
- latestSessionRecord?.cumulativeUsage,
11160
- args.currentUsage
11161
- ),
11162
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11163
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11164
- sessionId: args.sessionId,
11165
- sliceId: args.sliceId,
11166
- state: "completed",
11167
- piMessages: args.allMessages,
11168
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11169
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11170
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11171
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
11172
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
11173
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
11174
- } : {}
11175
- });
11176
- } catch (recordError) {
11177
- logSessionRecordError(
11178
- recordError,
11179
- "agent_turn_completed_session_record_failed",
11180
- args,
11181
- {
11182
- "app.ai.resume_slice_id": args.sliceId
11183
- },
11184
- "Failed to persist completed turn session record"
11185
- );
11186
- }
11187
- }
11188
- async function persistAuthPauseSessionRecord(args) {
11189
- const nextSliceId = args.currentSliceId + 1;
11190
- try {
11191
- const latestSessionRecord = await getAgentTurnSessionRecord(
11192
- args.conversationId,
11193
- args.sessionId
11194
- );
11195
- const piMessages = resumableBoundary(
11196
- args.messages,
11197
- latestSessionRecord?.piMessages
11198
- );
11199
- if (piMessages.length > 0 && !isContinuableBoundary(piMessages)) {
11200
- return void 0;
11201
- }
11202
- return await upsertAgentTurnSessionRecord({
11203
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11204
- conversationId: args.conversationId,
11205
- cumulativeDurationMs: addDurationMs(
11206
- latestSessionRecord?.cumulativeDurationMs,
11207
- args.currentDurationMs
11208
- ),
11209
- cumulativeUsage: addAgentTurnUsage(
11210
- latestSessionRecord?.cumulativeUsage,
11211
- args.currentUsage
11212
- ),
11213
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11214
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11215
- sessionId: args.sessionId,
11216
- sliceId: nextSliceId,
11217
- state: "awaiting_resume",
11218
- piMessages,
11219
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11220
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11221
- resumeReason: "auth",
11222
- resumedFromSliceId: args.currentSliceId,
11223
- errorMessage: args.errorMessage,
11224
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11225
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11226
- });
11227
- } catch (recordError) {
11228
- logSessionRecordError(
11229
- recordError,
11230
- "agent_turn_auth_resume_session_record_failed",
11231
- args,
11232
- {
11233
- "app.ai.resume_from_slice_id": args.currentSliceId,
11234
- "app.ai.resume_next_slice_id": nextSliceId
11235
- },
11236
- "Failed to persist auth session record before retry"
11237
- );
11238
- }
11239
- return void 0;
11240
- }
11241
- async function persistTimeoutSessionRecord(args) {
11242
- const nextSliceId = args.currentSliceId + 1;
11243
- try {
11244
- const latestSessionRecord = await getAgentTurnSessionRecord(
11245
- args.conversationId,
11246
- args.sessionId
11247
- );
11248
- const piMessages = resumableBoundary(
11249
- args.messages,
11250
- latestSessionRecord?.piMessages
11251
- );
11252
- if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11253
- return void 0;
11254
- }
11255
- const cumulativeDurationMs = addDurationMs(
11256
- latestSessionRecord?.cumulativeDurationMs,
11257
- args.currentDurationMs
11258
- );
11259
- const cumulativeUsage = addAgentTurnUsage(
11260
- latestSessionRecord?.cumulativeUsage,
11261
- args.currentUsage
11262
- );
11263
- if (nextSliceId > AGENT_CONTINUE_MAX_SLICES) {
11264
- return await upsertAgentTurnSessionRecord({
11265
- ...args.channelName ?? latestSessionRecord?.channelName ? {
11266
- channelName: args.channelName ?? latestSessionRecord?.channelName
11267
- } : {},
11268
- conversationId: args.conversationId,
11269
- cumulativeDurationMs,
11270
- cumulativeUsage,
11271
- ...args.destination ?? latestSessionRecord?.destination ? {
11272
- destination: args.destination ?? latestSessionRecord?.destination
11273
- } : {},
11274
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11275
- sessionId: args.sessionId,
11276
- sliceId: args.currentSliceId,
11277
- state: "failed",
11278
- piMessages,
11279
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11280
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11281
- resumeReason: "timeout",
11282
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
11283
- errorMessage: `Agent continuation exceeded slice limit (${AGENT_CONTINUE_MAX_SLICES})`,
11284
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11285
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11300
+ "</thread-background>",
11301
+ ""
11302
+ );
11303
+ }
11304
+ }
11305
+ sections.push(renderCurrentInstruction(args.messageText.trim() || "[empty]"));
11306
+ for (const block of args.currentTurnBlocks ?? []) {
11307
+ const trimmed = block.trim();
11308
+ if (!trimmed) {
11309
+ continue;
11310
+ }
11311
+ sections.push("", trimmed);
11312
+ }
11313
+ return sections.join("\n");
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
+ } : {}
11286
11366
  });
11367
+ return normalizedSelection;
11287
11368
  }
11288
- return await upsertAgentTurnSessionRecord({
11289
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11290
- conversationId: args.conversationId,
11291
- cumulativeDurationMs,
11292
- cumulativeUsage,
11293
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11294
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11295
- sessionId: args.sessionId,
11296
- sliceId: nextSliceId,
11297
- state: "awaiting_resume",
11298
- piMessages,
11299
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11300
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11301
- resumeReason: "timeout",
11302
- resumedFromSliceId: args.currentSliceId,
11303
- errorMessage: args.errorMessage,
11304
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11305
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
11306
- });
11307
- } catch (recordError) {
11308
- logSessionRecordError(
11309
- recordError,
11310
- "agent_continue_session_record_failed",
11311
- args,
11312
- {
11313
- "app.ai.resume_from_slice_id": args.currentSliceId,
11314
- "app.ai.resume_next_slice_id": nextSliceId
11315
- },
11316
- "Failed to persist session record before scheduling agent continuation"
11317
- );
11318
- 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;
11319
11375
  }
11376
+ return {
11377
+ ...selection,
11378
+ thinkingLevel: minimum,
11379
+ reason: `thinking_floor:${minimum}:${selection.reason}`
11380
+ };
11320
11381
  }
11321
- async function persistYieldSessionRecord(args) {
11382
+ async function classifyTurn(args) {
11322
11383
  try {
11323
- const latestSessionRecord = await getAgentTurnSessionRecord(
11324
- args.conversationId,
11325
- args.sessionId
11326
- );
11327
- const piMessages = resumableBoundary(
11328
- args.messages,
11329
- latestSessionRecord?.piMessages
11330
- );
11331
- if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
11332
- return void 0;
11333
- }
11334
- return await upsertAgentTurnSessionRecord({
11335
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
11336
- conversationId: args.conversationId,
11337
- cumulativeDurationMs: addDurationMs(
11338
- latestSessionRecord?.cumulativeDurationMs,
11339
- args.currentDurationMs
11340
- ),
11341
- cumulativeUsage: addAgentTurnUsage(
11342
- latestSessionRecord?.cumulativeUsage,
11343
- args.currentUsage
11344
- ),
11345
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
11346
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
11347
- sessionId: args.sessionId,
11348
- sliceId: args.currentSliceId,
11349
- state: "awaiting_resume",
11350
- piMessages,
11351
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
11352
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
11353
- resumeReason: "yield",
11354
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
11355
- errorMessage: args.errorMessage,
11356
- ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
11357
- ...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
11358
11393
  });
11359
- } catch (recordError) {
11360
- logSessionRecordError(
11361
- recordError,
11362
- "agent_turn_yield_session_record_failed",
11363
- args,
11364
- {
11365
- "app.ai.resume_slice_id": args.currentSliceId
11366
- },
11367
- "Failed to persist cooperative yield session record"
11368
- );
11369
- 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";
11370
11427
  }
11371
11428
  }
11372
11429
 
@@ -13051,7 +13108,10 @@ function legacyTextPartMatchesCurrentText(storedText, currentText) {
13051
13108
  async function generateAssistantReply(messageText2, context) {
13052
13109
  const conversationPrivacy = resolveConversationPrivacy({
13053
13110
  channelId: context.correlation?.channelId,
13054
- 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
13055
13115
  });
13056
13116
  return runWithConversationPrivacy(
13057
13117
  conversationPrivacy ?? "private",
@@ -13214,14 +13274,26 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13214
13274
  if (!turnSessionState.canUseTurnSession || !sessionConversationId || !sessionId) {
13215
13275
  return;
13216
13276
  }
13217
- await recordToolExecutionStarted({
13218
- conversationId: sessionConversationId,
13219
- sessionId,
13220
- toolCallId: event.toolCallId,
13221
- toolName: event.toolName,
13222
- args: event.args,
13223
- ttlMs: THREAD_STATE_TTL_MS4
13224
- });
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
+ }
13225
13297
  };
13226
13298
  const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
13227
13299
  configurationValues = {
@@ -13581,10 +13653,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13581
13653
  );
13582
13654
  const hasPromptCheckpoint = resumedFromSessionRecord && existingSessionRecord?.turnStartMessageIndex !== void 0;
13583
13655
  const shouldPromptAgent = !resumedFromSessionRecord || !hasPromptCheckpoint;
13584
- const promptHistoryMessages = shouldPromptAgent && resumedFromSessionRecord ? withoutTrailingUncheckpointedUserPrompt(
13656
+ const promptHistoryMessages = shouldPromptAgent ? withoutTrailingUncheckpointedUserPrompt(
13585
13657
  priorPiMessages,
13586
13658
  userContentParts
13587
- ) : shouldPromptAgent ? priorPiMessages ?? [] : existingSessionRecord.piMessages;
13659
+ ) : existingSessionRecord.piMessages;
13588
13660
  const needsBootstrapContextForPrompt = shouldPromptAgent && !hasRuntimeTurnContext(promptHistoryMessages);
13589
13661
  const systemPromptContributions = await getPluginSystemPromptContributions(toolSource);
13590
13662
  const pluginSystemPrompt = buildPluginSystemPromptContributions(
@@ -13739,12 +13811,12 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13739
13811
  });
13740
13812
  if (steeredMessageCount > 0) {
13741
13813
  logInfo(
13742
- "agent_turn_steering_messages_injected",
13814
+ "agent_turn_steering_messages_accepted",
13743
13815
  spanContext,
13744
13816
  {
13745
13817
  "app.ai.steering_message_count": steeredMessageCount
13746
13818
  },
13747
- "Agent turn steering messages injected"
13819
+ "Agent turn steering messages accepted"
13748
13820
  );
13749
13821
  }
13750
13822
  } catch (error) {
@@ -13835,10 +13907,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13835
13907
  beforeMessageCount = agent.state.messages.length;
13836
13908
  try {
13837
13909
  if (resumedFromSessionRecord) {
13838
- agent.state.messages = shouldPromptAgent ? promptHistoryMessages ?? [] : existingSessionRecord.piMessages;
13910
+ agent.state.messages = shouldPromptAgent ? promptHistoryMessages : existingSessionRecord.piMessages;
13839
13911
  turnStartMessageIndex = existingSessionRecord.turnStartMessageIndex;
13840
- } else if (context.piMessages && context.piMessages.length > 0) {
13841
- agent.state.messages = [...context.piMessages];
13912
+ } else if (promptHistoryMessages.length > 0) {
13913
+ agent.state.messages = [...promptHistoryMessages];
13842
13914
  }
13843
13915
  beforeMessageCount = agent.state.messages.length;
13844
13916
  if (shouldPromptAgent) {
@@ -14006,22 +14078,6 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
14006
14078
  }
14007
14079
  if (turnSessionState.canUseTurnSession && sessionConversationId && sessionId) {
14008
14080
  await recordActiveMcpProviders();
14009
- await persistCompletedSessionRecord({
14010
- channelName: context.correlation?.channelName,
14011
- conversationId: sessionConversationId,
14012
- currentDurationMs: Date.now() - replyStartedAtMs,
14013
- currentUsage: turnUsage,
14014
- destination: context.destination,
14015
- source: runSource,
14016
- sessionId,
14017
- sliceId: currentSliceId,
14018
- allMessages: agent.state.messages,
14019
- loadedSkillNames: loadedSkillNamesForResume,
14020
- logContext: sessionRecordLogContext,
14021
- requester,
14022
- ...surface ? { surface } : {},
14023
- ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
14024
- });
14025
14081
  }
14026
14082
  return buildTurnResult({
14027
14083
  newMessages,
@@ -14183,7 +14239,7 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
14183
14239
  );
14184
14240
  const message = error instanceof Error ? error.message : String(error);
14185
14241
  return {
14186
- text: `Error: ${message}`,
14242
+ text: "",
14187
14243
  ...getSandboxMetadata(),
14188
14244
  diagnostics: {
14189
14245
  outcome: "provider_error",
@@ -14574,6 +14630,98 @@ function getConversationMessageSlackTs(message) {
14574
14630
  return message.meta?.slackTs ?? toOptionalString(message.id);
14575
14631
  }
14576
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
+
14577
14725
  // src/chat/plugins/task-runner.ts
14578
14726
  import { pluginRunContextSchema } from "@sentry/junior-plugin-api";
14579
14727
  var PLUGIN_TASK_LOCK_TTL_MS = 5 * 60 * 1e3;
@@ -14849,13 +14997,19 @@ export {
14849
14997
  isCooperativeTurnYieldError,
14850
14998
  TurnInputCommitLostError,
14851
14999
  isTurnInputCommitLostError,
15000
+ TurnInputDeferredError,
15001
+ isTurnInputDeferredError,
14852
15002
  startActiveTurn,
14853
15003
  markTurnClosed,
14854
15004
  markTurnFailed,
14855
15005
  isRedundantReactionAckText,
15006
+ persistCompletedSessionRecord,
15007
+ completeDeliveredTurn,
15008
+ persistYieldSessionRecord,
14856
15009
  finalizeMcpAuthorization,
14857
15010
  resolvePluginOAuthAccount,
14858
15011
  executeCredentialedEgressRequest,
15012
+ buildSteeringPiMessage,
14859
15013
  generateAssistantReply,
14860
15014
  estimateTextTokens,
14861
15015
  getAgentContextCompactionTriggerTokens,
@@ -14869,6 +15023,9 @@ export {
14869
15023
  createConversationMemoryService,
14870
15024
  isHumanConversationMessage,
14871
15025
  getConversationMessageSlackTs,
15026
+ requireTurnFailureEventId,
15027
+ getAgentTurnDiagnosticsAttributes,
15028
+ finalizeFailedTurnReply,
14872
15029
  scheduleSessionCompletedPluginTasks,
14873
15030
  processPluginTask,
14874
15031
  buildDeliveredTurnStatePatch