@sentry/junior 0.85.0 → 0.87.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 (49) hide show
  1. package/dist/{agent-hooks-AV7CHLUO.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +779 -356
  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/no-reply.d.ts +7 -0
  9. package/dist/chat/respond.d.ts +7 -1
  10. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  11. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  12. package/dist/chat/runtime/turn.d.ts +7 -0
  13. package/dist/chat/services/persist-retry.d.ts +2 -0
  14. package/dist/chat/services/reply-delivery-plan.d.ts +0 -4
  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-UVNV67EC.js → chunk-2NFV5FMB.js} +4 -4
  27. package/dist/{chunk-QTTTRBNB.js → chunk-6I6HBOQM.js} +80 -15
  28. package/dist/{chunk-ENEWQMRJ.js → chunk-6O5UI3RG.js} +1 -1
  29. package/dist/{chunk-GY7L6VCT.js → chunk-ACJJJEGG.js} +35 -17
  30. package/dist/{chunk-MHEV3T2Y.js → chunk-B6FQPS7A.js} +953 -795
  31. package/dist/{chunk-MTE5NRPJ.js → chunk-BRSQQRG6.js} +1 -1
  32. package/dist/{chunk-EENOFKDN.js → chunk-ENPSU7L7.js} +165 -27
  33. package/dist/{chunk-RUB77TGS.js → chunk-GB5DFM4D.js} +1 -1
  34. package/dist/{chunk-C2PSUWK3.js → chunk-GGD6WK6V.js} +124 -25
  35. package/dist/{chunk-SSUEWAVS.js → chunk-JRXCSSSU.js} +19 -4
  36. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  37. package/dist/{chunk-FYPO4K7C.js → chunk-RIB3M6YA.js} +2 -2
  38. package/dist/{chunk-BU2AWPEN.js → chunk-ZU2ALUVQ.js} +3 -2
  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-S3HYBLUS.js → db-SZVUU7RB.js} +4 -4
  44. package/dist/instrumentation.js +1 -1
  45. package/dist/nitro.js +4 -4
  46. package/dist/reporting/conversations.d.ts +2 -1
  47. package/dist/reporting.js +95 -30
  48. package/dist/{runner-IMQH6V2B.js → runner-DMP3IQNM.js} +27 -13
  49. 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,
@@ -22,15 +22,19 @@ import {
22
22
  buildPluginSystemPromptContributions,
23
23
  buildSystemPrompt,
24
24
  buildTurnContextPrompt,
25
+ containsNoReplyMarker,
25
26
  getAgentTurnSessionRecord,
27
+ getInterruptionMarker,
28
+ isNoReplyMarker,
26
29
  loadConnectedMcpProviders,
27
30
  recordAuthorizationRequested,
28
31
  recordMcpProviderConnected,
29
32
  recordSubagentEnded,
30
33
  recordSubagentStarted,
31
34
  recordToolExecutionStarted,
35
+ stripNoReplyMarker,
32
36
  upsertAgentTurnSessionRecord
33
- } from "./chunk-GY7L6VCT.js";
37
+ } from "./chunk-ACJJJEGG.js";
34
38
  import {
35
39
  createPluginEmbedder,
36
40
  createPluginHookRunner,
@@ -41,14 +45,15 @@ import {
41
45
  getPlugins,
42
46
  getSlackToolContext,
43
47
  resolveChannelCapabilities
44
- } from "./chunk-UVNV67EC.js";
48
+ } from "./chunk-2NFV5FMB.js";
45
49
  import {
46
50
  createPluginLogger,
47
51
  createPluginState
48
- } from "./chunk-MTE5NRPJ.js";
52
+ } from "./chunk-BRSQQRG6.js";
49
53
  import {
54
+ getConversationStore,
50
55
  getDb
51
- } from "./chunk-QTTTRBNB.js";
56
+ } from "./chunk-6I6HBOQM.js";
52
57
  import {
53
58
  SANDBOX_DATA_ROOT,
54
59
  SANDBOX_SKILLS_ROOT,
@@ -59,7 +64,7 @@ import {
59
64
  import {
60
65
  acquireActiveLock,
61
66
  getStateAdapter
62
- } from "./chunk-RUB77TGS.js";
67
+ } from "./chunk-GB5DFM4D.js";
63
68
  import {
64
69
  SlackActionError,
65
70
  downloadPrivateSlackFile,
@@ -70,7 +75,7 @@ import {
70
75
  normalizeSlackConversationId,
71
76
  parseDestination,
72
77
  withSlackRetries
73
- } from "./chunk-C2PSUWK3.js";
78
+ } from "./chunk-GGD6WK6V.js";
74
79
  import {
75
80
  GEN_AI_PROVIDER_NAME,
76
81
  GEN_AI_SERVER_ADDRESS,
@@ -106,7 +111,7 @@ import {
106
111
  toObservablePromptPart,
107
112
  trimTrailingAssistantMessages,
108
113
  upsertActiveSkill
109
- } from "./chunk-BU2AWPEN.js";
114
+ } from "./chunk-ZU2ALUVQ.js";
110
115
  import {
111
116
  parseSlackThreadId,
112
117
  resolveConversationPrivacy,
@@ -116,7 +121,7 @@ import {
116
121
  toGenAiPayloadMetadata,
117
122
  toGenAiPayloadTraceAttributes,
118
123
  toGenAiTextMetadata
119
- } from "./chunk-DR75T7J3.js";
124
+ } from "./chunk-L7OHKDOX.js";
120
125
  import {
121
126
  discoverSkills,
122
127
  findSkillByName,
@@ -138,6 +143,7 @@ import {
138
143
  listReferenceFiles
139
144
  } from "./chunk-6APU57E6.js";
140
145
  import {
146
+ buildTurnFailureResponse,
141
147
  extractGenAiUsageAttributes,
142
148
  extractGenAiUsageSummary,
143
149
  getActiveTraceId,
@@ -650,6 +656,16 @@ var TurnInputCommitLostError = class extends Error {
650
656
  function isTurnInputCommitLostError(error) {
651
657
  return error instanceof TurnInputCommitLostError;
652
658
  }
659
+ var TurnInputDeferredError = class extends Error {
660
+ code = "turn_input_deferred";
661
+ constructor(message = "Turn input is deferred until the active resume ends") {
662
+ super(message);
663
+ this.name = "TurnInputDeferredError";
664
+ }
665
+ };
666
+ function isTurnInputDeferredError(error) {
667
+ return error instanceof TurnInputDeferredError;
668
+ }
653
669
  function startActiveTurn(args) {
654
670
  args.conversation.processing.activeTurnId = args.nextTurnId;
655
671
  args.updateConversationStats(args.conversation);
@@ -679,186 +695,593 @@ function markTurnFailed(args) {
679
695
  args.updateConversationStats(args.conversation);
680
696
  }
681
697
 
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
- );
698
+ // src/chat/usage.ts
699
+ var COMPONENT_USAGE_FIELDS = [
700
+ "inputTokens",
701
+ "outputTokens",
702
+ "cachedInputTokens",
703
+ "cacheCreationTokens"
704
+ ];
705
+ function hasAgentTurnUsage(usage) {
706
+ return Boolean(
707
+ usage && Object.values(usage).some(
708
+ (value) => typeof value === "number" && Number.isFinite(value)
709
+ )
710
+ );
711
+ }
712
+ function getFiniteCount(value) {
713
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
714
+ }
715
+ function getComponentTotal(usage) {
716
+ let total;
717
+ for (const field of COMPONENT_USAGE_FIELDS) {
718
+ const value = getFiniteCount(usage[field]);
719
+ if (value === void 0) continue;
720
+ total = (total ?? 0) + value;
721
+ }
722
+ return total;
723
+ }
724
+ function addAgentTurnUsage(...usages) {
725
+ const components = {};
726
+ let componentTotal;
727
+ let totalOnlyTokens;
728
+ for (const usage of usages) {
729
+ if (!usage) continue;
730
+ const usageComponentTotal = getComponentTotal(usage);
731
+ if (usageComponentTotal !== void 0) {
732
+ componentTotal = (componentTotal ?? 0) + usageComponentTotal;
733
+ for (const field of COMPONENT_USAGE_FIELDS) {
734
+ const value = getFiniteCount(usage[field]);
735
+ if (value === void 0) continue;
736
+ components[field] = (components[field] ?? 0) + value;
700
737
  }
701
- capabilityToProvider.set(capability, provider);
738
+ continue;
739
+ }
740
+ const totalTokens = getFiniteCount(usage.totalTokens);
741
+ if (totalTokens !== void 0) {
742
+ totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
702
743
  }
703
744
  }
704
- cachedCatalog = { signature, providers, capabilityToProvider };
705
- return cachedCatalog;
745
+ if (totalOnlyTokens !== void 0) {
746
+ return {
747
+ totalTokens: totalOnlyTokens + (componentTotal ?? 0)
748
+ };
749
+ }
750
+ return hasAgentTurnUsage(components) ? components : void 0;
706
751
  }
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
- {},
752
+
753
+ // src/chat/services/turn-session-record.ts
754
+ var AGENT_CONTINUE_MAX_SLICES = 48;
755
+ function logSessionRecordError(error, eventName, args, attributes, message) {
756
+ logException(
757
+ error,
758
+ eventName,
719
759
  {
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
760
+ slackThreadId: args.logContext.threadId,
761
+ slackUserId: args.logContext.requesterId,
762
+ slackChannelId: args.logContext.channelId,
763
+ runId: args.logContext.runId,
764
+ assistantUserName: args.logContext.assistantUserName,
765
+ modelId: args.logContext.modelId
725
766
  },
726
- "Loaded capability provider catalog"
767
+ {
768
+ "app.ai.resume_conversation_id": args.conversationId,
769
+ "app.ai.resume_session_id": args.sessionId,
770
+ ...attributes
771
+ },
772
+ message
727
773
  );
728
774
  }
729
-
730
- // src/chat/capabilities/router.ts
731
- var ProviderCredentialRouter = class {
732
- brokersByProvider;
733
- constructor(input) {
734
- this.brokersByProvider = input.brokersByProvider;
775
+ function addDurationMs(prior, current) {
776
+ const total = [prior, current].reduce((sum, value) => {
777
+ if (typeof value !== "number" || !Number.isFinite(value)) {
778
+ return sum;
779
+ }
780
+ return (sum ?? 0) + Math.max(0, Math.floor(value));
781
+ }, void 0);
782
+ return total;
783
+ }
784
+ function isContinuableBoundary(messages) {
785
+ const lastRole = getPiMessageRole(messages.at(-1));
786
+ return lastRole === "user" || lastRole === "toolResult";
787
+ }
788
+ function resumableBoundary(messages, fallbackMessages) {
789
+ const current = trimTrailingAssistantMessages(messages);
790
+ if (current.length > 0 && isContinuableBoundary(current)) {
791
+ return current;
735
792
  }
736
- async issue(input) {
737
- const broker = this.brokersByProvider[input.provider];
738
- if (!broker) {
793
+ return trimTrailingAssistantMessages(fallbackMessages ?? []);
794
+ }
795
+ async function loadTurnSessionRecord(ctx) {
796
+ const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
797
+ const existingSessionRecord = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionRecord(ctx.conversationId, ctx.sessionId) : void 0;
798
+ const hasAwaitingResumeRecord = Boolean(
799
+ existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
800
+ );
801
+ return {
802
+ canUseTurnSession,
803
+ resumedFromSessionRecord: hasAwaitingResumeRecord,
804
+ currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
805
+ existingSessionRecord
806
+ };
807
+ }
808
+ async function persistRunningSessionRecord(args) {
809
+ if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
810
+ return false;
811
+ }
812
+ try {
813
+ const latestSessionRecord = await getAgentTurnSessionRecord(
814
+ args.conversationId,
815
+ args.sessionId
816
+ );
817
+ await upsertAgentTurnSessionRecord({
818
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
819
+ conversationId: args.conversationId,
820
+ cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
821
+ cumulativeUsage: latestSessionRecord?.cumulativeUsage,
822
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
823
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
824
+ sessionId: args.sessionId,
825
+ sliceId: args.sliceId,
826
+ state: "running",
827
+ piMessages: args.messages,
828
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
829
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
830
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
831
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
832
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
833
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
834
+ } : {}
835
+ });
836
+ return true;
837
+ } catch (recordError) {
838
+ logSessionRecordError(
839
+ recordError,
840
+ "agent_turn_running_session_record_failed",
841
+ args,
842
+ {
843
+ "app.ai.resume_slice_id": args.sliceId
844
+ },
845
+ "Failed to persist running turn session record"
846
+ );
847
+ return false;
848
+ }
849
+ }
850
+ async function persistCompletedSessionRecord(args) {
851
+ let sliceId = args.sliceId;
852
+ try {
853
+ const latestSessionRecord = await getAgentTurnSessionRecord(
854
+ args.conversationId,
855
+ args.sessionId
856
+ );
857
+ sliceId = sliceId ?? latestSessionRecord?.sliceId;
858
+ if (sliceId === void 0) {
739
859
  throw new Error(
740
- `No credential broker registered for provider: ${input.provider}`
860
+ "Completed session record requires a slice id from the caller or the latest stored record"
741
861
  );
742
862
  }
743
- return await broker.issue({
744
- context: input.context,
745
- reason: input.reason
863
+ await upsertAgentTurnSessionRecord({
864
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
865
+ conversationId: args.conversationId,
866
+ cumulativeDurationMs: addDurationMs(
867
+ latestSessionRecord?.cumulativeDurationMs,
868
+ args.currentDurationMs
869
+ ),
870
+ cumulativeUsage: addAgentTurnUsage(
871
+ latestSessionRecord?.cumulativeUsage,
872
+ args.currentUsage
873
+ ),
874
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
875
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
876
+ ...args.destinationVisibility ? { destinationVisibility: args.destinationVisibility } : {},
877
+ sessionId: args.sessionId,
878
+ sliceId,
879
+ state: "completed",
880
+ piMessages: args.allMessages,
881
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
882
+ ...args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames ? {
883
+ loadedSkillNames: args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames
884
+ } : {},
885
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
886
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
887
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
888
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
889
+ } : {}
746
890
  });
891
+ } catch (recordError) {
892
+ logSessionRecordError(
893
+ recordError,
894
+ "agent_turn_completed_session_record_failed",
895
+ args,
896
+ sliceId !== void 0 ? { "app.ai.resume_slice_id": sliceId } : {},
897
+ "Failed to persist completed turn session record"
898
+ );
747
899
  }
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
900
  }
765
- function refreshLockKey(userId, provider) {
766
- return `${tokenKey(userId, provider)}:refresh`;
901
+ async function completeDeliveredTurn(args) {
902
+ await persistCompletedSessionRecord({
903
+ channelName: args.channelName,
904
+ conversationId: args.conversationId,
905
+ currentDurationMs: args.durationMs,
906
+ currentUsage: args.usage,
907
+ destination: args.destination,
908
+ destinationVisibility: args.destinationVisibility,
909
+ source: args.source,
910
+ sessionId: args.sessionId,
911
+ sliceId: args.sliceId,
912
+ allMessages: args.messages,
913
+ loadedSkillNames: args.loadedSkillNames,
914
+ logContext: args.logContext,
915
+ requester: args.requester,
916
+ surface: args.surface,
917
+ turnStartMessageIndex: args.turnStartMessageIndex
918
+ });
767
919
  }
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);
920
+ async function persistAuthPauseSessionRecord(args) {
921
+ const nextSliceId = args.currentSliceId + 1;
922
+ try {
923
+ const latestSessionRecord = await getAgentTurnSessionRecord(
924
+ args.conversationId,
925
+ args.sessionId
926
+ );
927
+ const piMessages = resumableBoundary(
928
+ args.messages,
929
+ latestSessionRecord?.piMessages
930
+ );
931
+ if (piMessages.length > 0 && !isContinuableBoundary(piMessages)) {
932
+ return void 0;
806
933
  }
934
+ return await upsertAgentTurnSessionRecord({
935
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
936
+ conversationId: args.conversationId,
937
+ cumulativeDurationMs: addDurationMs(
938
+ latestSessionRecord?.cumulativeDurationMs,
939
+ args.currentDurationMs
940
+ ),
941
+ cumulativeUsage: addAgentTurnUsage(
942
+ latestSessionRecord?.cumulativeUsage,
943
+ args.currentUsage
944
+ ),
945
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
946
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
947
+ sessionId: args.sessionId,
948
+ sliceId: nextSliceId,
949
+ state: "awaiting_resume",
950
+ piMessages,
951
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
952
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
953
+ resumeReason: "auth",
954
+ resumedFromSliceId: args.currentSliceId,
955
+ errorMessage: args.errorMessage,
956
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
957
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
958
+ });
959
+ } catch (recordError) {
960
+ logSessionRecordError(
961
+ recordError,
962
+ "agent_turn_auth_resume_session_record_failed",
963
+ args,
964
+ {
965
+ "app.ai.resume_from_slice_id": args.currentSliceId,
966
+ "app.ai.resume_next_slice_id": nextSliceId
967
+ },
968
+ "Failed to persist auth session record before retry"
969
+ );
807
970
  }
808
- };
809
-
810
- // src/chat/capabilities/factory.ts
811
- var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
812
- function createUserTokenStore() {
813
- return new StateAdapterTokenStore(getStateAdapter());
971
+ return void 0;
814
972
  }
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;
973
+ async function persistTimeoutSessionRecord(args) {
974
+ const nextSliceId = args.currentSliceId + 1;
975
+ try {
976
+ const latestSessionRecord = await getAgentTurnSessionRecord(
977
+ args.conversationId,
978
+ args.sessionId
979
+ );
980
+ const piMessages = resumableBoundary(
981
+ args.messages,
982
+ latestSessionRecord?.piMessages
983
+ );
984
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
985
+ return void 0;
822
986
  }
823
- brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
824
- userTokenStore
987
+ const cumulativeDurationMs = addDurationMs(
988
+ latestSessionRecord?.cumulativeDurationMs,
989
+ args.currentDurationMs
990
+ );
991
+ const cumulativeUsage = addAgentTurnUsage(
992
+ latestSessionRecord?.cumulativeUsage,
993
+ args.currentUsage
994
+ );
995
+ if (nextSliceId > AGENT_CONTINUE_MAX_SLICES) {
996
+ return await upsertAgentTurnSessionRecord({
997
+ ...args.channelName ?? latestSessionRecord?.channelName ? {
998
+ channelName: args.channelName ?? latestSessionRecord?.channelName
999
+ } : {},
1000
+ conversationId: args.conversationId,
1001
+ cumulativeDurationMs,
1002
+ cumulativeUsage,
1003
+ ...args.destination ?? latestSessionRecord?.destination ? {
1004
+ destination: args.destination ?? latestSessionRecord?.destination
1005
+ } : {},
1006
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1007
+ sessionId: args.sessionId,
1008
+ sliceId: args.currentSliceId,
1009
+ state: "failed",
1010
+ piMessages,
1011
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1012
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1013
+ resumeReason: "timeout",
1014
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1015
+ errorMessage: `Agent continuation exceeded slice limit (${AGENT_CONTINUE_MAX_SLICES})`,
1016
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1017
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1018
+ });
1019
+ }
1020
+ return await upsertAgentTurnSessionRecord({
1021
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1022
+ conversationId: args.conversationId,
1023
+ cumulativeDurationMs,
1024
+ cumulativeUsage,
1025
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1026
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1027
+ sessionId: args.sessionId,
1028
+ sliceId: nextSliceId,
1029
+ state: "awaiting_resume",
1030
+ piMessages,
1031
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1032
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1033
+ resumeReason: "timeout",
1034
+ resumedFromSliceId: args.currentSliceId,
1035
+ errorMessage: args.errorMessage,
1036
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1037
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
825
1038
  });
1039
+ } catch (recordError) {
1040
+ logSessionRecordError(
1041
+ recordError,
1042
+ "agent_continue_session_record_failed",
1043
+ args,
1044
+ {
1045
+ "app.ai.resume_from_slice_id": args.currentSliceId,
1046
+ "app.ai.resume_next_slice_id": nextSliceId
1047
+ },
1048
+ "Failed to persist session record before scheduling agent continuation"
1049
+ );
1050
+ return void 0;
826
1051
  }
827
- return new ProviderCredentialRouter({ brokersByProvider });
828
1052
  }
829
- function getSandboxEgressRouter() {
830
- const stateAdapter = getStateAdapter();
831
- let router = sandboxEgressRouters.get(stateAdapter);
832
- if (!router) {
833
- router = createProviderCredentialRouter(
834
- new StateAdapterTokenStore(stateAdapter)
1053
+ async function persistYieldSessionRecord(args) {
1054
+ try {
1055
+ const latestSessionRecord = await getAgentTurnSessionRecord(
1056
+ args.conversationId,
1057
+ args.sessionId
835
1058
  );
836
- sandboxEgressRouters.set(stateAdapter, router);
1059
+ const piMessages = resumableBoundary(
1060
+ args.messages,
1061
+ latestSessionRecord?.piMessages
1062
+ );
1063
+ if (piMessages.length === 0 || !isContinuableBoundary(piMessages)) {
1064
+ return void 0;
1065
+ }
1066
+ return await upsertAgentTurnSessionRecord({
1067
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1068
+ conversationId: args.conversationId,
1069
+ cumulativeDurationMs: addDurationMs(
1070
+ latestSessionRecord?.cumulativeDurationMs,
1071
+ args.currentDurationMs
1072
+ ),
1073
+ cumulativeUsage: addAgentTurnUsage(
1074
+ latestSessionRecord?.cumulativeUsage,
1075
+ args.currentUsage
1076
+ ),
1077
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1078
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1079
+ sessionId: args.sessionId,
1080
+ sliceId: args.currentSliceId,
1081
+ state: "awaiting_resume",
1082
+ piMessages,
1083
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1084
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1085
+ resumeReason: "yield",
1086
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1087
+ errorMessage: args.errorMessage,
1088
+ ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
1089
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1090
+ });
1091
+ } catch (recordError) {
1092
+ logSessionRecordError(
1093
+ recordError,
1094
+ "agent_turn_yield_session_record_failed",
1095
+ args,
1096
+ {
1097
+ "app.ai.resume_slice_id": args.currentSliceId
1098
+ },
1099
+ "Failed to persist cooperative yield session record"
1100
+ );
1101
+ return void 0;
837
1102
  }
838
- return router;
839
- }
840
- async function issueProviderCredentialLease(input) {
841
- return await getSandboxEgressRouter().issue(input);
842
1103
  }
843
1104
 
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) {
1105
+ // src/chat/respond.ts
1106
+ import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
1107
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
1108
+ import { z as z4 } from "zod";
1109
+
1110
+ // src/chat/capabilities/catalog.ts
1111
+ var cachedCatalog;
1112
+ function getCapabilityCatalog() {
1113
+ const signature = pluginCatalogRuntime.getSignature();
1114
+ if (cachedCatalog?.signature === signature) return cachedCatalog;
1115
+ const providers = pluginCatalogRuntime.getCapabilityProviders();
1116
+ const capabilityToProvider = /* @__PURE__ */ new Map();
1117
+ for (const provider of providers) {
1118
+ for (const capability of provider.capabilities) {
1119
+ if (capabilityToProvider.has(capability)) {
1120
+ throw new Error(
1121
+ `Duplicate capability registration for "${capability}"`
1122
+ );
1123
+ }
1124
+ capabilityToProvider.set(capability, provider);
1125
+ }
1126
+ }
1127
+ cachedCatalog = { signature, providers, capabilityToProvider };
1128
+ return cachedCatalog;
1129
+ }
1130
+ var catalogLogged = false;
1131
+ function logCapabilityCatalogLoadedOnce() {
1132
+ if (catalogLogged) return;
1133
+ catalogLogged = true;
1134
+ const { providers } = getCapabilityCatalog();
1135
+ const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
1136
+ const configKeys = [
1137
+ ...new Set(providers.flatMap((p) => p.configKeys))
1138
+ ].sort();
1139
+ logInfo(
1140
+ "capability_catalog_loaded",
1141
+ {},
1142
+ {
1143
+ "app.capability.providers": providers.map((p) => p.provider),
1144
+ "app.capability.count": capabilityNames.length,
1145
+ "app.capability.names": capabilityNames,
1146
+ "app.config.key_count": configKeys.length,
1147
+ "app.config.keys": configKeys
1148
+ },
1149
+ "Loaded capability provider catalog"
1150
+ );
1151
+ }
1152
+
1153
+ // src/chat/capabilities/router.ts
1154
+ var ProviderCredentialRouter = class {
1155
+ brokersByProvider;
1156
+ constructor(input) {
1157
+ this.brokersByProvider = input.brokersByProvider;
1158
+ }
1159
+ async issue(input) {
1160
+ const broker = this.brokersByProvider[input.provider];
1161
+ if (!broker) {
1162
+ throw new Error(
1163
+ `No credential broker registered for provider: ${input.provider}`
1164
+ );
1165
+ }
1166
+ return await broker.issue({
1167
+ context: input.context,
1168
+ reason: input.reason
1169
+ });
1170
+ }
1171
+ };
1172
+
1173
+ // src/chat/credentials/user-token-store.ts
1174
+ import {
1175
+ pluginStoredTokensSchema
1176
+ } from "@sentry/junior-plugin-api";
1177
+ var storedTokensSchema = pluginStoredTokensSchema;
1178
+
1179
+ // src/chat/credentials/state-adapter-token-store.ts
1180
+ var KEY_PREFIX = "oauth-token";
1181
+ var BUFFER_MS = 24 * 60 * 60 * 1e3;
1182
+ var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
1183
+ var REFRESH_LOCK_WAIT_MS = 3e4;
1184
+ var REFRESH_LOCK_RETRY_MS = 100;
1185
+ function tokenKey(userId, provider) {
1186
+ return `${KEY_PREFIX}:${userId}:${provider}`;
1187
+ }
1188
+ function refreshLockKey(userId, provider) {
1189
+ return `${tokenKey(userId, provider)}:refresh`;
1190
+ }
1191
+ async function sleep(ms) {
1192
+ await new Promise((resolve) => setTimeout(resolve, ms));
1193
+ }
1194
+ var StateAdapterTokenStore = class {
1195
+ state;
1196
+ constructor(stateAdapter) {
1197
+ this.state = stateAdapter;
1198
+ }
1199
+ async get(userId, provider) {
1200
+ const stored = await this.state.get(tokenKey(userId, provider));
1201
+ return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
1202
+ }
1203
+ async set(userId, provider, tokens) {
1204
+ const parsed = storedTokensSchema.parse(tokens);
1205
+ const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
1206
+ const ttlMs2 = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
1207
+ await this.state.set(tokenKey(userId, provider), parsed, ttlMs2);
1208
+ }
1209
+ async delete(userId, provider) {
1210
+ await this.state.delete(tokenKey(userId, provider));
1211
+ }
1212
+ /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
1213
+ async withRefresh(userId, provider, callback) {
1214
+ const lockKey = refreshLockKey(userId, provider);
1215
+ const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
1216
+ while (true) {
1217
+ const lock = await acquireActiveLock(this.state, lockKey);
1218
+ if (lock) {
1219
+ try {
1220
+ return await callback();
1221
+ } finally {
1222
+ await this.state.releaseLock(lock);
1223
+ }
1224
+ }
1225
+ if (Date.now() >= deadline) {
1226
+ throw new Error(`Could not acquire OAuth token refresh lock`);
1227
+ }
1228
+ await sleep(REFRESH_LOCK_RETRY_MS);
1229
+ }
1230
+ }
1231
+ };
1232
+
1233
+ // src/chat/capabilities/factory.ts
1234
+ var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
1235
+ function createUserTokenStore() {
1236
+ return new StateAdapterTokenStore(getStateAdapter());
1237
+ }
1238
+ function createProviderCredentialRouter(userTokenStore) {
1239
+ logCapabilityCatalogLoadedOnce();
1240
+ const brokersByProvider = {};
1241
+ for (const plugin of pluginCatalogRuntime.getProviders()) {
1242
+ const { name } = plugin.manifest;
1243
+ if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
1244
+ continue;
1245
+ }
1246
+ brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
1247
+ userTokenStore
1248
+ });
1249
+ }
1250
+ return new ProviderCredentialRouter({ brokersByProvider });
1251
+ }
1252
+ function getSandboxEgressRouter() {
1253
+ const stateAdapter = getStateAdapter();
1254
+ let router = sandboxEgressRouters.get(stateAdapter);
1255
+ if (!router) {
1256
+ router = createProviderCredentialRouter(
1257
+ new StateAdapterTokenStore(stateAdapter)
1258
+ );
1259
+ sandboxEgressRouters.set(stateAdapter, router);
1260
+ }
1261
+ return router;
1262
+ }
1263
+ async function issueProviderCredentialLease(input) {
1264
+ return await getSandboxEgressRouter().issue(input);
1265
+ }
1266
+
1267
+ // src/chat/capabilities/jr-rpc-command.ts
1268
+ import { Bash, defineCommand } from "just-bash";
1269
+ function commandResult(input) {
1270
+ let stdout = "";
1271
+ if (typeof input.stdout === "string") {
1272
+ stdout = input.stdout;
1273
+ } else if (input.stdout !== void 0) {
1274
+ stdout = `${JSON.stringify(input.stdout, null, 2)}
1275
+ `;
1276
+ }
1277
+ return {
1278
+ stdout,
1279
+ stderr: input.stderr ?? "",
1280
+ exitCode: input.exitCode
1281
+ };
1282
+ }
1283
+ function requireChannelConfiguration(deps) {
1284
+ if (deps.channelConfiguration) {
862
1285
  return { ok: true, configuration: deps.channelConfiguration };
863
1286
  }
864
1287
  return {
@@ -4470,6 +4893,7 @@ async function getPermalinkBestEffort(args) {
4470
4893
  3,
4471
4894
  {
4472
4895
  action: "chat.getPermalink",
4896
+ idempotent: true,
4473
4897
  spanAttributes: {
4474
4898
  "app.slack.channel_id": args.channelId,
4475
4899
  "app.slack.message_ts": args.messageTs
@@ -4522,30 +4946,6 @@ async function postSlackMessage(input) {
4522
4946
  } : {}
4523
4947
  };
4524
4948
  }
4525
- async function deleteSlackMessage(input) {
4526
- const channelId = requireSlackConversationId(
4527
- input.channelId,
4528
- "Slack message deletion"
4529
- );
4530
- const timestamp = requireSlackMessageTimestamp(
4531
- input.timestamp,
4532
- "Slack message deletion"
4533
- );
4534
- await withSlackRetries(
4535
- () => getSlackClient().chat.delete({
4536
- channel: channelId,
4537
- ts: timestamp
4538
- }),
4539
- 3,
4540
- {
4541
- action: "chat.delete",
4542
- spanAttributes: {
4543
- "app.slack.channel_id": channelId,
4544
- "app.slack.message_ts": timestamp
4545
- }
4546
- }
4547
- );
4548
- }
4549
4949
  async function postSlackEphemeralMessage(input) {
4550
4950
  const channelId = requireSlackConversationId(
4551
4951
  input.channelId,
@@ -4647,6 +5047,7 @@ async function addReactionToMessage(input) {
4647
5047
  3,
4648
5048
  {
4649
5049
  action: "reactions.add",
5050
+ idempotent: true,
4650
5051
  spanAttributes: {
4651
5052
  "app.slack.channel_id": channelId,
4652
5053
  "app.slack.message_ts": timestamp,
@@ -4685,6 +5086,7 @@ async function removeReactionFromMessage(input) {
4685
5086
  3,
4686
5087
  {
4687
5088
  action: "reactions.remove",
5089
+ idempotent: true,
4688
5090
  spanAttributes: {
4689
5091
  "app.slack.channel_id": channelId,
4690
5092
  "app.slack.message_ts": timestamp,
@@ -4720,7 +5122,7 @@ function createOperationKey(toolName, input) {
4720
5122
  // src/chat/tools/slack/channel-post-message.ts
4721
5123
  function createSlackChannelPostMessageTool(context, state) {
4722
5124
  return tool({
4723
- description: "Post a new top-level message to the current Slack channel. Use only when the user explicitly asks to post/send/share/say something to the channel. Do not use for thread replies, inline @mentions, or pinging mentioned users.",
5125
+ description: "Post a new top-level message to the current Slack channel. Use only when the user explicitly asks to post/send/share/say something to the current channel. Do not use for other named channels, thread replies, inline @mentions, or pinging mentioned users.",
4724
5126
  inputSchema: Type15.Object({
4725
5127
  text: Type15.String({
4726
5128
  minLength: 1,
@@ -4765,7 +5167,7 @@ function createSlackChannelPostMessageTool(context, state) {
4765
5167
  import { Type as Type16 } from "@sinclair/typebox";
4766
5168
  function createSlackMessageAddReactionTool(context, state) {
4767
5169
  return tool({
4768
- description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
5170
+ description: "Add an emoji reaction to the current inbound Slack message. Use when the user asks for a reaction on the current message without another target. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
4769
5171
  inputSchema: Type16.Object({
4770
5172
  emoji: Type16.String({
4771
5173
  minLength: 1,
@@ -4777,17 +5179,15 @@ function createSlackMessageAddReactionTool(context, state) {
4777
5179
  const targetChannelId = context.sourceChannelId;
4778
5180
  const targetMessageTs = context.messageTs;
4779
5181
  if (!targetMessageTs) {
4780
- return {
4781
- ok: false,
4782
- error: "No active message timestamp is available for reactions"
4783
- };
5182
+ throw new ToolInputError(
5183
+ "No active message timestamp is available for reactions."
5184
+ );
4784
5185
  }
4785
5186
  const normalizedEmoji = normalizeSlackEmojiName(emoji);
4786
5187
  if (!normalizedEmoji) {
4787
- return {
4788
- ok: false,
4789
- error: "Emoji must be a valid Slack emoji alias name (for example `thumbsup` or `thumbsup::skin-tone-6`)"
4790
- };
5188
+ throw new ToolInputError(
5189
+ "Emoji must be a valid Slack emoji alias name (for example `thumbsup` or `thumbsup::skin-tone-6`)."
5190
+ );
4791
5191
  }
4792
5192
  const operationKey = createOperationKey("slackMessageAddReaction", {
4793
5193
  channel_id: targetChannelId,
@@ -5750,6 +6150,31 @@ function createSlackListUpdateItemTool(state) {
5750
6150
  // src/chat/tools/slack/thread-read.ts
5751
6151
  import { Type as Type19 } from "@sinclair/typebox";
5752
6152
 
6153
+ // src/chat/tools/slack/channel-access.ts
6154
+ async function checkSlackChannelReadAccess(args) {
6155
+ const target = normalizeSlackConversationId(args.targetChannelId);
6156
+ if (!target) {
6157
+ return { allowed: false, error: "Invalid Slack channel ID." };
6158
+ }
6159
+ const currentChannels = args.currentChannelIds.map((channelId) => normalizeSlackConversationId(channelId)).filter((channelId) => Boolean(channelId));
6160
+ if (currentChannels.includes(target)) {
6161
+ return { allowed: true };
6162
+ }
6163
+ const store = args.store ?? getConversationStore();
6164
+ const visibility = await store.getDestinationVisibility({
6165
+ provider: "slack",
6166
+ providerTenantId: args.teamId,
6167
+ providerDestinationId: target
6168
+ });
6169
+ if (visibility === "public") {
6170
+ return { allowed: true };
6171
+ }
6172
+ return {
6173
+ allowed: false,
6174
+ error: "Cannot read this Slack conversation: only the current conversation or public channels Junior has seen in this workspace are readable."
6175
+ };
6176
+ }
6177
+
5753
6178
  // src/chat/tools/slack/slack-message-url.ts
5754
6179
  var SLACK_HOST_PATTERN = /^[a-z0-9-]+\.slack(?:-gov)?\.com$/;
5755
6180
  var ARCHIVE_PATH_PATTERN = /^\/archives\/([CDG][A-Z0-9]+)\/p(\d{10})(\d{6})$/;
@@ -5909,26 +6334,9 @@ function truncateMessages(messages, maxChars) {
5909
6334
  }
5910
6335
  return { messages: kept, omitted: messages.length - kept.length };
5911
6336
  }
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) {
6337
+ function createSlackThreadReadTool(context, deps = {}) {
5930
6338
  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.",
6339
+ 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
6340
  annotations: { readOnlyHint: true, destructiveHint: false },
5933
6341
  inputSchema: Type19.Object({
5934
6342
  url: Type19.Optional(
@@ -5988,10 +6396,15 @@ function createSlackThreadReadTool(context) {
5988
6396
  error: "Provide either a Slack message `url` or both `channel_id` and `ts`."
5989
6397
  };
5990
6398
  }
5991
- const access = checkChannelAccess(
5992
- channelId,
5993
- context.destinationChannelId
5994
- );
6399
+ const access = await checkSlackChannelReadAccess({
6400
+ currentChannelIds: [
6401
+ context.destinationChannelId,
6402
+ context.sourceChannelId
6403
+ ],
6404
+ store: deps.visibilityStore,
6405
+ targetChannelId: channelId,
6406
+ teamId: context.teamId
6407
+ });
5995
6408
  if (!access.allowed) {
5996
6409
  return {
5997
6410
  ok: false,
@@ -10518,33 +10931,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10518
10931
  }
10519
10932
 
10520
10933
  // src/chat/services/reply-delivery-plan.ts
10521
- var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
10522
- var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
10523
- var REACTION_INTENT_RE = /\b(react|reaction|emoji|thumbs?\s*up|acknowledge)\b/i;
10524
- var REACTION_WITH_REPLY_INTENT_RE = /\b(confirm|explain|reply|respond|say|tell|summari[sz]e|why)\b/i;
10525
- function normalizeReactionAckText(text) {
10526
- return text.trim().toLowerCase().replace(/[!.]+$/g, "");
10527
- }
10528
- function isRedundantReactionAckText(text) {
10529
- const trimmed = text.trim();
10530
- if (!trimmed) {
10531
- return false;
10532
- }
10533
- if (REACTION_ONLY_ACK_RE.test(trimmed)) {
10534
- return true;
10535
- }
10536
- const normalized = normalizeReactionAckText(text);
10537
- return REDUNDANT_REACTION_ACK_TEXT.includes(
10538
- normalized
10539
- );
10540
- }
10541
- function isReactionOnlyIntent(text) {
10542
- const normalized = text.trim();
10543
- if (!normalized) {
10544
- return false;
10545
- }
10546
- return REACTION_INTENT_RE.test(normalized) && !REACTION_WITH_REPLY_INTENT_RE.test(normalized);
10547
- }
10548
10934
  function buildReplyDeliveryPlan(args) {
10549
10935
  const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
10550
10936
  let attachFiles = "none";
@@ -10658,9 +11044,12 @@ function buildTurnResult(input) {
10658
11044
  const toolResults = newMessages.filter(isToolResultMessage);
10659
11045
  const assistantMessages = newMessages.filter(isAssistantMessage);
10660
11046
  const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
10661
- const primaryText = stripThinkingXmlBlocks(
11047
+ const rawPrimaryText = stripThinkingXmlBlocks(
10662
11048
  terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n")
10663
11049
  ).trim();
11050
+ const exactNoReplyMarker = isNoReplyMarker(rawPrimaryText);
11051
+ const mixedNoReplyMarker = !exactNoReplyMarker && containsNoReplyMarker(rawPrimaryText);
11052
+ const primaryText = exactNoReplyMarker ? "" : mixedNoReplyMarker ? stripNoReplyMarker(rawPrimaryText) : rawPrimaryText;
10664
11053
  const toolErrorCount = toolResults.filter((result) => result.isError).length;
10665
11054
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
10666
11055
  const successfulToolNames = new Set(
@@ -10671,19 +11060,51 @@ function buildTurnResult(input) {
10671
11060
  );
10672
11061
  const canvasCreated = successfulToolNames.has("slackCanvasCreate");
10673
11062
  const reactionPerformed = successfulToolNames.has("slackMessageAddReaction");
11063
+ const markerSideEffectSuccess = exactNoReplyMarker && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
11064
+ const fileOnlySuccess = !rawPrimaryText && toolErrorCount === 0 && replyFiles.length > 0;
11065
+ const sideEffectOnlySuccess = markerSideEffectSuccess || fileOnlySuccess;
10674
11066
  const baseDeliveryPlan = buildReplyDeliveryPlan({
10675
- explicitChannelPostIntent,
11067
+ explicitChannelPostIntent: exactNoReplyMarker && explicitChannelPostIntent,
10676
11068
  channelPostPerformed,
10677
11069
  hasFiles: replyFiles.length > 0
10678
11070
  });
10679
- const sideEffectOnlySuccess = !primaryText && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
10680
11071
  const lastAssistant = terminalAssistantMessages.at(-1);
10681
11072
  const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
10682
11073
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
10683
11074
  const isProviderError = stopReason === "error";
10684
- if (!primaryText && !sideEffectOnlySuccess && !isProviderError) {
11075
+ if (exactNoReplyMarker) {
11076
+ const markerCategory = reactionPerformed ? "reaction" : channelPostPerformed ? "channel_post" : replyFiles.length > 0 ? "file" : "none";
11077
+ const markerContext = {
11078
+ slackThreadId: correlation?.threadId,
11079
+ slackUserId: correlation?.requesterId,
11080
+ slackChannelId: correlation?.channelId,
11081
+ runId: correlation?.runId,
11082
+ assistantUserName,
11083
+ modelId: botConfig.modelId
11084
+ };
11085
+ const markerAttributes = {
11086
+ "app.ai.no_reply_marker": true,
11087
+ "app.ai.no_reply_marker_category": markerCategory,
11088
+ "app.ai.no_reply_marker_accepted": sideEffectOnlySuccess && !isProviderError
11089
+ };
11090
+ if (sideEffectOnlySuccess && !isProviderError) {
11091
+ logInfo(
11092
+ "ai_no_reply_marker_accepted",
11093
+ markerContext,
11094
+ markerAttributes,
11095
+ "No-reply marker suppressed visible thread text"
11096
+ );
11097
+ } else if (!isProviderError) {
11098
+ logWarn(
11099
+ "ai_no_reply_marker_rejected",
11100
+ markerContext,
11101
+ markerAttributes,
11102
+ "No-reply marker requires a successful visible side effect"
11103
+ );
11104
+ }
11105
+ } else if (mixedNoReplyMarker) {
10685
11106
  logWarn(
10686
- "ai_model_response_empty",
11107
+ "ai_no_reply_marker_mixed_text",
10687
11108
  {
10688
11109
  slackThreadId: correlation?.threadId,
10689
11110
  slackUserId: correlation?.requesterId,
@@ -10693,14 +11114,32 @@ function buildTurnResult(input) {
10693
11114
  modelId: botConfig.modelId
10694
11115
  },
10695
11116
  {
10696
- "app.ai.tool_results": toolResults.length,
10697
- "app.ai.tool_error_results": toolErrorCount,
10698
- "app.ai.generated_files": input.generatedFileCount
11117
+ "app.ai.no_reply_marker": true,
11118
+ "app.ai.no_reply_marker_mode": "mixed"
10699
11119
  },
10700
- "Model returned empty text response"
11120
+ "No-reply marker appeared with visible assistant text"
10701
11121
  );
10702
11122
  }
10703
- const usedPrimaryText = Boolean(primaryText);
11123
+ if (!primaryText && !sideEffectOnlySuccess && !isProviderError && !exactNoReplyMarker) {
11124
+ logWarn(
11125
+ "ai_model_response_empty",
11126
+ {
11127
+ slackThreadId: correlation?.threadId,
11128
+ slackUserId: correlation?.requesterId,
11129
+ slackChannelId: correlation?.channelId,
11130
+ runId: correlation?.runId,
11131
+ assistantUserName,
11132
+ modelId: botConfig.modelId
11133
+ },
11134
+ {
11135
+ "app.ai.tool_results": toolResults.length,
11136
+ "app.ai.tool_error_results": toolErrorCount,
11137
+ "app.ai.generated_files": input.generatedFileCount
11138
+ },
11139
+ "Model returned empty text response"
11140
+ );
11141
+ }
11142
+ const usedPrimaryText = Boolean(rawPrimaryText);
10704
11143
  let outcome;
10705
11144
  if (isProviderError) {
10706
11145
  outcome = "provider_error";
@@ -10709,8 +11148,7 @@ function buildTurnResult(input) {
10709
11148
  } else {
10710
11149
  outcome = "execution_failure";
10711
11150
  }
10712
- const suppressReactionOnlyText = reactionPerformed && !channelPostPerformed && replyFiles.length === 0 && Boolean(primaryText) && isReactionOnlyIntent(userInput);
10713
- const rawResponseText = suppressReactionOnlyText ? "" : primaryText;
11151
+ const rawResponseText = primaryText;
10714
11152
  const responseText = canvasCreated && isVerbosePostCanvasReply(rawResponseText) ? buildBriefPostCanvasReply(artifactStatePatch) : rawResponseText;
10715
11153
  const escapedOrRawPayload = Boolean(primaryText) && (isExecutionEscapeResponse(primaryText) || isRawToolPayloadResponse(primaryText));
10716
11154
  const resolvedText = escapedOrRawPayload ? "" : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
@@ -10860,513 +11298,133 @@ function buildClassifierPrompt(args) {
10860
11298
  sections.push(
10861
11299
  "<thread-background>",
10862
11300
  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 } : {}
11301
+ "</thread-background>",
11302
+ ""
11303
+ );
11304
+ }
11305
+ }
11306
+ sections.push(renderCurrentInstruction(args.messageText.trim() || "[empty]"));
11307
+ for (const block of args.currentTurnBlocks ?? []) {
11308
+ const trimmed = block.trim();
11309
+ if (!trimmed) {
11310
+ continue;
11311
+ }
11312
+ sections.push("", trimmed);
11313
+ }
11314
+ return sections.join("\n");
11315
+ }
11316
+ async function selectTurnThinkingLevel(args) {
11317
+ const trimmedContext = trimContextForRouter(args.conversationContext);
11318
+ const instructionLength = args.messageText.trim().length;
11319
+ const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
11320
+ (block) => block.trim().length > 0
11321
+ ).length;
11322
+ const prompt = buildClassifierPrompt({
11323
+ conversationContext: trimmedContext,
11324
+ currentTurnBlocks: args.currentTurnBlocks,
11325
+ messageText: args.messageText
11326
+ });
11327
+ const logContext = {
11328
+ slackThreadId: args.context?.threadId,
11329
+ slackChannelId: args.context?.channelId,
11330
+ slackUserId: args.context?.requesterId,
11331
+ runId: args.context?.runId,
11332
+ modelId: args.fastModelId
11333
+ };
11334
+ return withSpan(
11335
+ "chat.route_thinking",
11336
+ "chat.route_thinking",
11337
+ logContext,
11338
+ async () => {
11339
+ setSpanAttributes({
11340
+ "app.ai.router.prompt_char_count": prompt.length,
11341
+ "app.ai.router.instruction_char_count": instructionLength,
11342
+ "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
11343
+ "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
11344
+ "app.ai.router.turn_block_count": turnBlockCount
11345
+ });
11346
+ const selection = await classifyTurn({
11347
+ completeObject: args.completeObject,
11348
+ fastModelId: args.fastModelId,
11349
+ metadata: {
11350
+ modelId: args.fastModelId,
11351
+ threadId: args.context?.threadId ?? "",
11352
+ channelId: args.context?.channelId ?? "",
11353
+ requesterId: args.context?.requesterId ?? "",
11354
+ runId: args.context?.runId ?? ""
11355
+ },
11356
+ prompt
11357
+ });
11358
+ const normalizedSelection = applyThinkingFloor(selection, {
11359
+ minimum: trimmedContext || turnBlockCount > 0 ? "medium" : void 0
11360
+ });
11361
+ setSpanAttributes({
11362
+ "app.ai.thinking_level": normalizedSelection.thinkingLevel,
11363
+ "app.ai.thinking_level_reason": normalizedSelection.reason,
11364
+ ...normalizedSelection.confidence !== void 0 ? {
11365
+ "app.ai.thinking_level_confidence": normalizedSelection.confidence
11366
+ } : {}
11286
11367
  });
11368
+ return normalizedSelection;
11287
11369
  }
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;
11370
+ );
11371
+ }
11372
+ function applyThinkingFloor(selection, args) {
11373
+ const minimum = args.minimum;
11374
+ if (!minimum || selection.thinkingLevel === "none" || THINKING_LEVEL_RANK[selection.thinkingLevel] >= THINKING_LEVEL_RANK[minimum]) {
11375
+ return selection;
11319
11376
  }
11377
+ return {
11378
+ ...selection,
11379
+ thinkingLevel: minimum,
11380
+ reason: `thinking_floor:${minimum}:${selection.reason}`
11381
+ };
11320
11382
  }
11321
- async function persistYieldSessionRecord(args) {
11383
+ async function classifyTurn(args) {
11322
11384
  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 } : {}
11385
+ const result = await args.completeObject({
11386
+ modelId: args.fastModelId,
11387
+ schema: turnExecutionProfileSchema,
11388
+ maxTokens: 120,
11389
+ metadata: args.metadata,
11390
+ prompt: args.prompt,
11391
+ thinkingLevel: "low",
11392
+ system: buildClassifierSystemPrompt(),
11393
+ temperature: 0
11358
11394
  });
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;
11395
+ const parsed = turnExecutionProfileSchema.parse(result.object);
11396
+ const reason = parsed.reason.trim();
11397
+ if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
11398
+ return {
11399
+ confidence: parsed.confidence,
11400
+ thinkingLevel: DEFAULT_THINKING_LEVEL,
11401
+ reason: `low_confidence_medium_default:${reason}`
11402
+ };
11403
+ }
11404
+ return {
11405
+ confidence: parsed.confidence,
11406
+ thinkingLevel: parsed.thinking_level,
11407
+ reason
11408
+ };
11409
+ } catch {
11410
+ return {
11411
+ thinkingLevel: DEFAULT_THINKING_LEVEL,
11412
+ reason: "classifier_error_default"
11413
+ };
11414
+ }
11415
+ }
11416
+ function toAgentThinkingLevel(level) {
11417
+ switch (level) {
11418
+ case "none":
11419
+ return "off";
11420
+ case "low":
11421
+ return "low";
11422
+ case "medium":
11423
+ return "medium";
11424
+ case "high":
11425
+ return "high";
11426
+ case "xhigh":
11427
+ return "xhigh";
11370
11428
  }
11371
11429
  }
11372
11430
 
@@ -13051,7 +13109,10 @@ function legacyTextPartMatchesCurrentText(storedText, currentText) {
13051
13109
  async function generateAssistantReply(messageText2, context) {
13052
13110
  const conversationPrivacy = resolveConversationPrivacy({
13053
13111
  channelId: context.correlation?.channelId,
13054
- conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId
13112
+ conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
13113
+ // Source-confirmed visibility from the live event's channel_type; without
13114
+ // it the turn fails closed to private telemetry capture.
13115
+ visibility: context.slackConversation?.visibility
13055
13116
  });
13056
13117
  return runWithConversationPrivacy(
13057
13118
  conversationPrivacy ?? "private",
@@ -13214,14 +13275,26 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13214
13275
  if (!turnSessionState.canUseTurnSession || !sessionConversationId || !sessionId) {
13215
13276
  return;
13216
13277
  }
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
- });
13278
+ try {
13279
+ await recordToolExecutionStarted({
13280
+ conversationId: sessionConversationId,
13281
+ sessionId,
13282
+ toolCallId: event.toolCallId,
13283
+ toolName: event.toolName,
13284
+ args: event.args,
13285
+ ttlMs: THREAD_STATE_TTL_MS4
13286
+ });
13287
+ } catch (error) {
13288
+ logException(
13289
+ error,
13290
+ "agent_turn_session_log_append_failed",
13291
+ spanContext,
13292
+ {
13293
+ "gen_ai.tool.name": event.toolName
13294
+ },
13295
+ "Failed to record host-only tool execution start"
13296
+ );
13297
+ }
13225
13298
  };
13226
13299
  const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
13227
13300
  configurationValues = {
@@ -13581,10 +13654,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13581
13654
  );
13582
13655
  const hasPromptCheckpoint = resumedFromSessionRecord && existingSessionRecord?.turnStartMessageIndex !== void 0;
13583
13656
  const shouldPromptAgent = !resumedFromSessionRecord || !hasPromptCheckpoint;
13584
- const promptHistoryMessages = shouldPromptAgent && resumedFromSessionRecord ? withoutTrailingUncheckpointedUserPrompt(
13657
+ const promptHistoryMessages = shouldPromptAgent ? withoutTrailingUncheckpointedUserPrompt(
13585
13658
  priorPiMessages,
13586
13659
  userContentParts
13587
- ) : shouldPromptAgent ? priorPiMessages ?? [] : existingSessionRecord.piMessages;
13660
+ ) : existingSessionRecord.piMessages;
13588
13661
  const needsBootstrapContextForPrompt = shouldPromptAgent && !hasRuntimeTurnContext(promptHistoryMessages);
13589
13662
  const systemPromptContributions = await getPluginSystemPromptContributions(toolSource);
13590
13663
  const pluginSystemPrompt = buildPluginSystemPromptContributions(
@@ -13739,12 +13812,12 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13739
13812
  });
13740
13813
  if (steeredMessageCount > 0) {
13741
13814
  logInfo(
13742
- "agent_turn_steering_messages_injected",
13815
+ "agent_turn_steering_messages_accepted",
13743
13816
  spanContext,
13744
13817
  {
13745
13818
  "app.ai.steering_message_count": steeredMessageCount
13746
13819
  },
13747
- "Agent turn steering messages injected"
13820
+ "Agent turn steering messages accepted"
13748
13821
  );
13749
13822
  }
13750
13823
  } catch (error) {
@@ -13835,10 +13908,10 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
13835
13908
  beforeMessageCount = agent.state.messages.length;
13836
13909
  try {
13837
13910
  if (resumedFromSessionRecord) {
13838
- agent.state.messages = shouldPromptAgent ? promptHistoryMessages ?? [] : existingSessionRecord.piMessages;
13911
+ agent.state.messages = shouldPromptAgent ? promptHistoryMessages : existingSessionRecord.piMessages;
13839
13912
  turnStartMessageIndex = existingSessionRecord.turnStartMessageIndex;
13840
- } else if (context.piMessages && context.piMessages.length > 0) {
13841
- agent.state.messages = [...context.piMessages];
13913
+ } else if (promptHistoryMessages.length > 0) {
13914
+ agent.state.messages = [...promptHistoryMessages];
13842
13915
  }
13843
13916
  beforeMessageCount = agent.state.messages.length;
13844
13917
  if (shouldPromptAgent) {
@@ -14006,22 +14079,6 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
14006
14079
  }
14007
14080
  if (turnSessionState.canUseTurnSession && sessionConversationId && sessionId) {
14008
14081
  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
14082
  }
14026
14083
  return buildTurnResult({
14027
14084
  newMessages,
@@ -14183,7 +14240,7 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
14183
14240
  );
14184
14241
  const message = error instanceof Error ? error.message : String(error);
14185
14242
  return {
14186
- text: `Error: ${message}`,
14243
+ text: "",
14187
14244
  ...getSandboxMetadata(),
14188
14245
  diagnostics: {
14189
14246
  outcome: "provider_error",
@@ -14574,6 +14631,98 @@ function getConversationMessageSlackTs(message) {
14574
14631
  return message.meta?.slackTs ?? toOptionalString(message.id);
14575
14632
  }
14576
14633
 
14634
+ // src/chat/services/turn-failure-response.ts
14635
+ function requireTurnFailureEventId(eventId, eventName) {
14636
+ if (!eventId) {
14637
+ throw new Error(`Sentry did not return an event ID for ${eventName}`);
14638
+ }
14639
+ return eventId;
14640
+ }
14641
+ function getExecutionFailureReason(reply) {
14642
+ const errorMessage = reply.diagnostics.errorMessage?.trim();
14643
+ if (errorMessage) {
14644
+ return errorMessage;
14645
+ }
14646
+ if (reply.diagnostics.toolErrorCount > 0) {
14647
+ return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
14648
+ }
14649
+ if (reply.diagnostics.assistantMessageCount > 0) {
14650
+ return "assistant returned no text";
14651
+ }
14652
+ return "empty assistant turn";
14653
+ }
14654
+ function getFailureCapture(reply) {
14655
+ if (reply.diagnostics.outcome === "provider_error") {
14656
+ return {
14657
+ eventName: "agent_turn_provider_error",
14658
+ error: reply.diagnostics.providerError ?? new Error(
14659
+ reply.diagnostics.errorMessage ?? "Provider error without explicit message"
14660
+ ),
14661
+ attributes: {},
14662
+ body: "Agent turn failed with provider error"
14663
+ };
14664
+ }
14665
+ const failureReason = getExecutionFailureReason(reply);
14666
+ return {
14667
+ eventName: "agent_turn_execution_failure",
14668
+ error: new Error(`Agent turn execution failure: ${failureReason}`),
14669
+ attributes: {
14670
+ "app.ai.execution_failure_reason": failureReason
14671
+ },
14672
+ body: "Agent turn completed with execution failure"
14673
+ };
14674
+ }
14675
+ function getAgentTurnDiagnosticsAttributes(reply) {
14676
+ return {
14677
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
14678
+ "gen_ai.operation.name": "invoke_agent",
14679
+ "app.ai.outcome": reply.diagnostics.outcome,
14680
+ "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
14681
+ "app.ai.tool_results": reply.diagnostics.toolResultCount,
14682
+ "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
14683
+ "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
14684
+ "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
14685
+ ...reply.diagnostics.thinkingLevel ? {
14686
+ "app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
14687
+ } : {},
14688
+ ...reply.diagnostics.stopReason ? {
14689
+ "gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
14690
+ } : {},
14691
+ ...reply.diagnostics.errorMessage ? { "exception.message": reply.diagnostics.errorMessage } : {}
14692
+ };
14693
+ }
14694
+ function finalizeFailedTurnReply(args) {
14695
+ if (args.reply.diagnostics.outcome === "success") {
14696
+ return args.reply;
14697
+ }
14698
+ const capture = getFailureCapture(args.reply);
14699
+ const eventId = requireTurnFailureEventId(
14700
+ args.logException(
14701
+ capture.error,
14702
+ capture.eventName,
14703
+ args.context,
14704
+ {
14705
+ ...getAgentTurnDiagnosticsAttributes(args.reply),
14706
+ ...args.attributes,
14707
+ ...capture.attributes
14708
+ },
14709
+ capture.body
14710
+ ),
14711
+ capture.eventName
14712
+ );
14713
+ const providerPartialText = args.reply.diagnostics.outcome === "provider_error" && args.reply.diagnostics.assistantMessageCount > 0 ? args.reply.text.trim() : "";
14714
+ return {
14715
+ ...args.reply,
14716
+ text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
14717
+ deliveryMode: "thread",
14718
+ deliveryPlan: {
14719
+ mode: "thread",
14720
+ postThreadText: true,
14721
+ attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
14722
+ }
14723
+ };
14724
+ }
14725
+
14577
14726
  // src/chat/plugins/task-runner.ts
14578
14727
  import { pluginRunContextSchema } from "@sentry/junior-plugin-api";
14579
14728
  var PLUGIN_TASK_LOCK_TTL_MS = 5 * 60 * 1e3;
@@ -14763,6 +14912,7 @@ async function processPluginTask(message, options = {}) {
14763
14912
  }
14764
14913
 
14765
14914
  // src/chat/runtime/delivered-turn-state.ts
14915
+ var NO_VISIBLE_REPLY_CONVERSATION_TEXT = "[no visible reply]";
14766
14916
  function buildDeliveredTurnStatePatch(args) {
14767
14917
  const conversation = structuredClone(args.conversation);
14768
14918
  const artifactStatePatch = {
@@ -14771,6 +14921,7 @@ function buildDeliveredTurnStatePatch(args) {
14771
14921
  };
14772
14922
  const artifacts = Object.keys(artifactStatePatch).length > 0 ? mergeArtifactsState(args.artifacts, artifactStatePatch) : void 0;
14773
14923
  clearPendingAuth(conversation, args.sessionId);
14924
+ const assistantText = normalizeConversationText(args.reply.text) || (args.reply.deliveryPlan?.postThreadText === false ? NO_VISIBLE_REPLY_CONVERSATION_TEXT : "[empty response]");
14774
14925
  markConversationMessage(conversation, args.userMessageId, {
14775
14926
  replied: true,
14776
14927
  skippedReason: void 0
@@ -14778,7 +14929,7 @@ function buildDeliveredTurnStatePatch(args) {
14778
14929
  upsertConversationMessage(conversation, {
14779
14930
  id: generateConversationId("assistant"),
14780
14931
  role: "assistant",
14781
- text: normalizeConversationText(args.reply.text) || "[empty response]",
14932
+ text: assistantText,
14782
14933
  createdAtMs: Date.now(),
14783
14934
  author: {
14784
14935
  userName: botConfig.userName,
@@ -14810,7 +14961,6 @@ export {
14810
14961
  deliverResourceEventSubscription,
14811
14962
  listThreadReplies,
14812
14963
  postSlackMessage,
14813
- deleteSlackMessage,
14814
14964
  uploadFilesToThread,
14815
14965
  addReactionToMessage,
14816
14966
  removeReactionFromMessage,
@@ -14849,13 +14999,18 @@ export {
14849
14999
  isCooperativeTurnYieldError,
14850
15000
  TurnInputCommitLostError,
14851
15001
  isTurnInputCommitLostError,
15002
+ TurnInputDeferredError,
15003
+ isTurnInputDeferredError,
14852
15004
  startActiveTurn,
14853
15005
  markTurnClosed,
14854
15006
  markTurnFailed,
14855
- isRedundantReactionAckText,
15007
+ persistCompletedSessionRecord,
15008
+ completeDeliveredTurn,
15009
+ persistYieldSessionRecord,
14856
15010
  finalizeMcpAuthorization,
14857
15011
  resolvePluginOAuthAccount,
14858
15012
  executeCredentialedEgressRequest,
15013
+ buildSteeringPiMessage,
14859
15014
  generateAssistantReply,
14860
15015
  estimateTextTokens,
14861
15016
  getAgentContextCompactionTriggerTokens,
@@ -14869,6 +15024,9 @@ export {
14869
15024
  createConversationMemoryService,
14870
15025
  isHumanConversationMessage,
14871
15026
  getConversationMessageSlackTs,
15027
+ requireTurnFailureEventId,
15028
+ getAgentTurnDiagnosticsAttributes,
15029
+ finalizeFailedTurnReply,
14872
15030
  scheduleSessionCompletedPluginTasks,
14873
15031
  processPluginTask,
14874
15032
  buildDeliveredTurnStatePatch