@sentry/junior 0.55.0 → 0.56.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.
package/dist/app.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  runNonInteractiveCommand,
33
33
  sandboxSkillDir,
34
34
  sandboxSkillFile
35
- } from "./chunk-XI3CFWTA.js";
35
+ } from "./chunk-AA5TIFN5.js";
36
36
  import {
37
37
  CredentialUnavailableError,
38
38
  buildOAuthTokenRequest,
@@ -10745,6 +10745,21 @@ function parseEnv(raw) {
10745
10745
  Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
10746
10746
  );
10747
10747
  }
10748
+ function sandboxStreamInterruptedResult(toolName) {
10749
+ return {
10750
+ content: [
10751
+ {
10752
+ type: "text",
10753
+ text: `Sandbox command stream was interrupted during ${toolName}. The operation did not complete reliably. It may have produced side effects; inspect the workspace or retry only if it is safe.`
10754
+ }
10755
+ ],
10756
+ details: {
10757
+ ok: false,
10758
+ error: "stream_interrupted",
10759
+ tool: toolName
10760
+ }
10761
+ };
10762
+ }
10748
10763
  function createSandboxExecutor(options) {
10749
10764
  let availableSkills = [];
10750
10765
  let referenceFiles = [];
@@ -11096,23 +11111,30 @@ function createSandboxExecutor(options) {
11096
11111
  }
11097
11112
  return await executeBashTool(rawInput, bashCommand);
11098
11113
  }
11099
- if (params.toolName === "readFile") {
11100
- return await executeReadFileTool(rawInput);
11101
- }
11102
- if (params.toolName === "editFile") {
11103
- return await executeEditFileTool(rawInput);
11104
- }
11105
- if (params.toolName === "grep") {
11106
- return await executeGrepTool(rawInput);
11107
- }
11108
- if (params.toolName === "findFiles") {
11109
- return await executeFindFilesTool(rawInput);
11110
- }
11111
- if (params.toolName === "listDir") {
11112
- return await executeListDirTool(rawInput);
11113
- }
11114
- if (params.toolName === "writeFile") {
11115
- return await executeWriteFileTool(rawInput);
11114
+ try {
11115
+ if (params.toolName === "readFile") {
11116
+ return await executeReadFileTool(rawInput);
11117
+ }
11118
+ if (params.toolName === "editFile") {
11119
+ return await executeEditFileTool(rawInput);
11120
+ }
11121
+ if (params.toolName === "grep") {
11122
+ return await executeGrepTool(rawInput);
11123
+ }
11124
+ if (params.toolName === "findFiles") {
11125
+ return await executeFindFilesTool(rawInput);
11126
+ }
11127
+ if (params.toolName === "listDir") {
11128
+ return await executeListDirTool(rawInput);
11129
+ }
11130
+ if (params.toolName === "writeFile") {
11131
+ return await executeWriteFileTool(rawInput);
11132
+ }
11133
+ } catch (error) {
11134
+ if (!isSandboxCommandStreamInterruptedError(error)) {
11135
+ throw error;
11136
+ }
11137
+ return { result: sandboxStreamInterruptedResult(params.toolName) };
11116
11138
  }
11117
11139
  throw new Error(`unsupported sandbox tool: ${params.toolName}`);
11118
11140
  };
@@ -14087,7 +14109,7 @@ function buildUserTurnInput(args) {
14087
14109
  }
14088
14110
  return { routerBlocks, userContentParts };
14089
14111
  }
14090
- async function generateAssistantReply(messageText, context = {}) {
14112
+ async function generateAssistantReply(messageText2, context = {}) {
14091
14113
  const replyStartedAtMs = Date.now();
14092
14114
  let timeoutResumeConversationId;
14093
14115
  let timeoutResumeSessionId;
@@ -14156,7 +14178,7 @@ async function generateAssistantReply(messageText, context = {}) {
14156
14178
  }
14157
14179
  let baseInstructions = "";
14158
14180
  let configurationValues;
14159
- const userInput = messageText;
14181
+ const userInput = messageText2;
14160
14182
  if (shouldTrace) {
14161
14183
  const inboundAttachmentCount = context.inboundAttachmentCount ?? 0;
14162
14184
  const promptAttachmentCount = context.userAttachments?.length ?? 0;
@@ -14886,9 +14908,56 @@ async function generateAssistantReply(messageText, context = {}) {
14886
14908
  }
14887
14909
  }
14888
14910
 
14911
+ // src/chat/services/context-budget.ts
14912
+ var COMPACTION_TRIGGER_INPUT_RATIO = 0.75;
14913
+ var COMPACTION_OUTPUT_RESERVE_RATIO = 0.25;
14914
+ var COMPACTION_TARGET_RATIO = 0.8;
14915
+ var FALLBACK_CONTEXT_WINDOW_TOKENS = 4e5;
14916
+ var FALLBACK_MAX_OUTPUT_TOKENS = 128e3;
14917
+ function positiveInteger2(value, fallback) {
14918
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
14919
+ }
14920
+ function estimateTextTokens(text) {
14921
+ return Math.ceil(text.length / 4);
14922
+ }
14923
+ function calculateContextCompactionTriggerTokens(model) {
14924
+ const contextWindow = positiveInteger2(
14925
+ model.contextWindow,
14926
+ FALLBACK_CONTEXT_WINDOW_TOKENS
14927
+ );
14928
+ const maxTokens = positiveInteger2(
14929
+ model.maxTokens,
14930
+ FALLBACK_MAX_OUTPUT_TOKENS
14931
+ );
14932
+ const outputReserve = Math.min(
14933
+ maxTokens,
14934
+ Math.floor(contextWindow * COMPACTION_OUTPUT_RESERVE_RATIO)
14935
+ );
14936
+ const usableInputTokens = Math.max(1, contextWindow - outputReserve);
14937
+ return Math.max(
14938
+ 1,
14939
+ Math.floor(usableInputTokens * COMPACTION_TRIGGER_INPUT_RATIO)
14940
+ );
14941
+ }
14942
+ function calculateContextCompactionTargetTokens(triggerTokens) {
14943
+ return Math.max(1, Math.floor(triggerTokens * COMPACTION_TARGET_RATIO));
14944
+ }
14945
+ function getAgentContextCompactionTriggerTokens() {
14946
+ const model = resolveGatewayModel(botConfig.modelId);
14947
+ return calculateContextCompactionTriggerTokens({
14948
+ contextWindow: botConfig.modelContextWindowTokens ?? model.contextWindow,
14949
+ maxTokens: model.maxTokens
14950
+ });
14951
+ }
14952
+ function getConversationContextCompactionTriggerTokens() {
14953
+ const model = resolveGatewayModel(botConfig.fastModelId);
14954
+ return calculateContextCompactionTriggerTokens({
14955
+ contextWindow: model.contextWindow,
14956
+ maxTokens: model.maxTokens
14957
+ });
14958
+ }
14959
+
14889
14960
  // src/chat/services/conversation-memory.ts
14890
- var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
14891
- var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
14892
14961
  var CONTEXT_MIN_LIVE_MESSAGES = 12;
14893
14962
  var CONTEXT_COMPACTION_BATCH_SIZE = 24;
14894
14963
  var CONTEXT_MAX_COMPACTIONS = 16;
@@ -14899,9 +14968,6 @@ function generateConversationId(prefix) {
14899
14968
  function normalizeConversationText(text) {
14900
14969
  return text.trim().replace(/\s+/g, " ").slice(0, CONTEXT_MAX_MESSAGE_CHARS);
14901
14970
  }
14902
- function estimateTokenCount(text) {
14903
- return Math.ceil(text.length / 4);
14904
- }
14905
14971
  function buildImageContextSuffix(message, conversation) {
14906
14972
  const byFileId = conversation?.vision.byFileId;
14907
14973
  const imageFileIds = message.meta?.imageFileIds ?? [];
@@ -14931,7 +14997,7 @@ function renderConversationMessageLine(message, conversation) {
14931
14997
  }
14932
14998
  function updateConversationStats(conversation) {
14933
14999
  const contextText = buildConversationContext(conversation);
14934
- conversation.stats.estimatedContextTokens = estimateTokenCount(
15000
+ conversation.stats.estimatedContextTokens = estimateTextTokens(
14935
15001
  contextText ?? ""
14936
15002
  );
14937
15003
  conversation.stats.totalMessageCount = conversation.messages.length;
@@ -15128,7 +15194,9 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
15128
15194
  setSpanAttributes({
15129
15195
  "app.context_tokens_estimated": estimatedTokens
15130
15196
  });
15131
- while (estimatedTokens > CONTEXT_COMPACTION_TRIGGER_TOKENS && conversation.messages.length > CONTEXT_MIN_LIVE_MESSAGES) {
15197
+ const triggerTokens = getConversationContextCompactionTriggerTokens();
15198
+ const targetTokens = calculateContextCompactionTargetTokens(triggerTokens);
15199
+ while (estimatedTokens > triggerTokens && conversation.messages.length > CONTEXT_MIN_LIVE_MESSAGES) {
15132
15200
  const compactCount = Math.min(
15133
15201
  CONTEXT_COMPACTION_BATCH_SIZE,
15134
15202
  conversation.messages.length - CONTEXT_MIN_LIVE_MESSAGES
@@ -15156,9 +15224,11 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
15156
15224
  estimatedTokens = conversation.stats.estimatedContextTokens;
15157
15225
  setSpanAttributes({
15158
15226
  "app.compaction_messages_covered": compactCount,
15227
+ "app.compaction.trigger_tokens": triggerTokens,
15228
+ "app.compaction.target_tokens": targetTokens,
15159
15229
  "app.context_tokens_estimated": estimatedTokens
15160
15230
  });
15161
- if (estimatedTokens <= CONTEXT_COMPACTION_TARGET_TOKENS) {
15231
+ if (estimatedTokens <= targetTokens) {
15162
15232
  break;
15163
15233
  }
15164
15234
  }
@@ -16815,9 +16885,13 @@ function createAssistantStatusScheduler(args) {
16815
16885
  }
16816
16886
  };
16817
16887
  return {
16818
- start() {
16888
+ start(status) {
16819
16889
  active = true;
16820
16890
  clearPending();
16891
+ if (status) {
16892
+ void postRenderedStatus(status);
16893
+ return;
16894
+ }
16821
16895
  currentKey = "initial";
16822
16896
  void postStatus(getInitialStatusText(), loadingMessages);
16823
16897
  },
@@ -17731,13 +17805,13 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
17731
17805
  const currentState = await getPersistedThreadState(threadId);
17732
17806
  const conversation = coerceThreadConversationState(currentState);
17733
17807
  const artifacts = coerceThreadArtifactsState(currentState);
17734
- const userMessage = getTurnUserMessage(conversation, sessionId);
17808
+ const userMessage2 = getTurnUserMessage(conversation, sessionId);
17735
17809
  const statePatch = buildDeliveredTurnStatePatch({
17736
17810
  artifacts,
17737
17811
  conversation,
17738
17812
  reply,
17739
17813
  sessionId,
17740
- userMessageId: userMessage?.id
17814
+ userMessageId: userMessage2?.id
17741
17815
  });
17742
17816
  await persistThreadStateById(threadId, {
17743
17817
  ...statePatch
@@ -17800,7 +17874,7 @@ async function resumeAuthorizedMcpTurn(args) {
17800
17874
  requesterId: authSession.userId
17801
17875
  });
17802
17876
  const resolvedSessionId = pendingAuth?.sessionId ?? authSession.sessionId;
17803
- const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
17877
+ const userMessage2 = getTurnUserMessage(conversation, resolvedSessionId);
17804
17878
  if (pendingAuth) {
17805
17879
  if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
17806
17880
  clearPendingAuth(conversation, pendingAuth.sessionId);
@@ -17815,14 +17889,14 @@ async function resumeAuthorizedMcpTurn(args) {
17815
17889
  } else if (conversation.processing.activeTurnId !== authSession.sessionId) {
17816
17890
  return;
17817
17891
  }
17818
- if (!userMessage) {
17892
+ if (!userMessage2) {
17819
17893
  return;
17820
17894
  }
17821
17895
  await resumeAuthorizedRequest({
17822
- messageText: userMessage.text,
17896
+ messageText: userMessage2.text,
17823
17897
  channelId: authSession.channelId,
17824
17898
  threadTs: authSession.threadTs,
17825
- messageTs: getTurnUserSlackMessageTs(userMessage),
17899
+ messageTs: getTurnUserSlackMessageTs(userMessage2),
17826
17900
  lockKey: threadId,
17827
17901
  connectedText: "",
17828
17902
  beforeStart: async () => {
@@ -18188,13 +18262,13 @@ async function persistCompletedOAuthReplyState(args) {
18188
18262
  const currentState = await getPersistedThreadState(args.conversationId);
18189
18263
  const conversation = coerceThreadConversationState(currentState);
18190
18264
  const artifacts = coerceThreadArtifactsState(currentState);
18191
- const userMessage = getTurnUserMessage(conversation, args.sessionId);
18265
+ const userMessage2 = getTurnUserMessage(conversation, args.sessionId);
18192
18266
  const statePatch = buildDeliveredTurnStatePatch({
18193
18267
  artifacts,
18194
18268
  conversation,
18195
18269
  reply: args.reply,
18196
18270
  sessionId: args.sessionId,
18197
- userMessageId: userMessage?.id
18271
+ userMessageId: userMessage2?.id
18198
18272
  });
18199
18273
  await persistThreadStateById(args.conversationId, {
18200
18274
  ...statePatch
@@ -18269,7 +18343,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
18269
18343
  requesterId: stored.userId
18270
18344
  });
18271
18345
  const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
18272
- const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
18346
+ const userMessage2 = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
18273
18347
  if (pendingAuth) {
18274
18348
  if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
18275
18349
  clearPendingAuth(conversation, pendingAuth.sessionId);
@@ -18284,21 +18358,21 @@ async function resumeCheckpointedOAuthTurn(stored) {
18284
18358
  return true;
18285
18359
  }
18286
18360
  } else {
18287
- if (!userMessage?.author?.userId) {
18361
+ if (!userMessage2?.author?.userId) {
18288
18362
  return false;
18289
18363
  }
18290
18364
  if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
18291
18365
  return true;
18292
18366
  }
18293
18367
  }
18294
- if (!userMessage?.author?.userId || !resolvedSessionId) {
18368
+ if (!userMessage2?.author?.userId || !resolvedSessionId) {
18295
18369
  return false;
18296
18370
  }
18297
18371
  await resumeSlackTurn({
18298
- messageText: stored.pendingMessage ?? userMessage.text,
18372
+ messageText: stored.pendingMessage ?? userMessage2.text,
18299
18373
  channelId: stored.channelId,
18300
18374
  threadTs: stored.threadTs,
18301
- messageTs: getTurnUserSlackMessageTs(userMessage),
18375
+ messageTs: getTurnUserSlackMessageTs(userMessage2),
18302
18376
  lockKey: stored.resumeConversationId,
18303
18377
  initialText: "",
18304
18378
  beforeStart: async () => {
@@ -19229,7 +19303,7 @@ async function persistCompletedReplyState2(args) {
19229
19303
  );
19230
19304
  const conversation = coerceThreadConversationState(currentState);
19231
19305
  const artifacts = coerceThreadArtifactsState(currentState);
19232
- const userMessage = getTurnUserMessage(
19306
+ const userMessage2 = getTurnUserMessage(
19233
19307
  conversation,
19234
19308
  args.checkpoint.sessionId
19235
19309
  );
@@ -19238,7 +19312,7 @@ async function persistCompletedReplyState2(args) {
19238
19312
  conversation,
19239
19313
  reply: args.reply,
19240
19314
  sessionId: args.checkpoint.sessionId,
19241
- userMessageId: userMessage?.id
19315
+ userMessageId: userMessage2?.id
19242
19316
  });
19243
19317
  await persistThreadStateById(args.checkpoint.conversationId, {
19244
19318
  ...statePatch
@@ -19310,8 +19384,8 @@ async function resumeTimedOutTurn(payload) {
19310
19384
  );
19311
19385
  const conversation = coerceThreadConversationState(currentState);
19312
19386
  const artifacts = coerceThreadArtifactsState(currentState);
19313
- const userMessage = getTurnUserMessage(conversation, payload.sessionId);
19314
- if (!userMessage?.author?.userId) {
19387
+ const userMessage2 = getTurnUserMessage(conversation, payload.sessionId);
19388
+ if (!userMessage2?.author?.userId) {
19315
19389
  throw new Error(
19316
19390
  `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
19317
19391
  );
@@ -19323,24 +19397,24 @@ async function resumeTimedOutTurn(payload) {
19323
19397
  thread.channelId
19324
19398
  );
19325
19399
  const conversationContext = buildConversationContext(conversation, {
19326
- excludeMessageId: userMessage.id
19400
+ excludeMessageId: userMessage2.id
19327
19401
  });
19328
19402
  const sandbox = getPersistedSandboxState(currentState);
19329
19403
  return {
19330
- messageText: userMessage.text,
19331
- messageTs: getTurnUserSlackMessageTs(userMessage),
19404
+ messageText: userMessage2.text,
19405
+ messageTs: getTurnUserSlackMessageTs(userMessage2),
19332
19406
  replyContext: {
19333
19407
  requester: {
19334
- userId: userMessage.author.userId,
19335
- userName: userMessage.author.userName,
19336
- fullName: userMessage.author.fullName
19408
+ userId: userMessage2.author.userId,
19409
+ userName: userMessage2.author.userName,
19410
+ fullName: userMessage2.author.fullName
19337
19411
  },
19338
19412
  correlation: {
19339
19413
  conversationId: payload.conversationId,
19340
19414
  turnId: payload.sessionId,
19341
19415
  channelId: thread.channelId,
19342
19416
  threadTs: thread.threadTs,
19343
- requesterId: userMessage.author.userId
19417
+ requesterId: userMessage2.author.userId
19344
19418
  },
19345
19419
  toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
19346
19420
  artifactState: artifacts,
@@ -19359,7 +19433,7 @@ async function resumeTimedOutTurn(payload) {
19359
19433
  conversation
19360
19434
  });
19361
19435
  },
19362
- ...getTurnUserReplyAttachmentContext(userMessage)
19436
+ ...getTurnUserReplyAttachmentContext(userMessage2)
19363
19437
  },
19364
19438
  onSuccess: async (reply) => {
19365
19439
  await persistCompletedReplyState2({ checkpoint, reply });
@@ -20178,6 +20252,294 @@ function createSlackTurnRuntime(deps) {
20178
20252
  };
20179
20253
  }
20180
20254
 
20255
+ // src/chat/services/context-compaction.ts
20256
+ import {
20257
+ estimateContextTokens,
20258
+ estimateTokens
20259
+ } from "@earendil-works/pi-agent-core";
20260
+ var RETAINED_USER_MESSAGE_TOKENS = 2e4;
20261
+ var MAX_SUMMARY_INPUT_CHARS = 8e4;
20262
+ var MAX_VISIBLE_CONTEXT_CHARS = 2e4;
20263
+ var MAX_SUMMARY_CHARS = 6e3;
20264
+ var MAX_RENDERED_MESSAGE_CHARS = 4e3;
20265
+ var COMPACTION_SUMMARY_PREFIX = "Context handoff summary for future Junior turns:";
20266
+ var OMITTED_OLDER_CONTEXT_NOTICE = "[older context omitted]";
20267
+ function textPart(value) {
20268
+ if (value && typeof value === "object" && value.type === "text" && typeof value.text === "string") {
20269
+ return value.text;
20270
+ }
20271
+ return void 0;
20272
+ }
20273
+ function messageText(message) {
20274
+ const content = message.content;
20275
+ if (!Array.isArray(content)) {
20276
+ return typeof content === "string" ? content : "";
20277
+ }
20278
+ return content.map(textPart).filter(Boolean).join("\n").trim();
20279
+ }
20280
+ function sanitizeText(text) {
20281
+ return text.replace(
20282
+ /<data_base64>[\s\S]*?<\/data_base64>/g,
20283
+ "<data_base64>[omitted]</data_base64>"
20284
+ ).replace(
20285
+ /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=]+/gi,
20286
+ "[image data omitted]"
20287
+ ).replaceAll("\0", " ").trim();
20288
+ }
20289
+ function truncateToTokenBudget(text, maxTokens) {
20290
+ const maxChars = Math.max(0, maxTokens * 4);
20291
+ if (text.length <= maxChars) {
20292
+ return text;
20293
+ }
20294
+ return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
20295
+ }
20296
+ function isCompactionSummary(text) {
20297
+ return text.trimStart().startsWith(COMPACTION_SUMMARY_PREFIX);
20298
+ }
20299
+ function isPayloadHeavy(text) {
20300
+ return /<data_base64>[\s\S]*?<\/data_base64>|data:image\/[a-z0-9.+-]+;base64,/i.test(
20301
+ text
20302
+ );
20303
+ }
20304
+ function userMessage(text) {
20305
+ return {
20306
+ role: "user",
20307
+ content: [{ type: "text", text }],
20308
+ timestamp: Date.now()
20309
+ };
20310
+ }
20311
+ function selectRetainedUserMessages(messages, maxTokens = RETAINED_USER_MESSAGE_TOKENS) {
20312
+ const stripped = stripRuntimeTurnContext(messages);
20313
+ const selected = [];
20314
+ let remaining = maxTokens;
20315
+ for (const message of [...stripped].reverse()) {
20316
+ if (message.role !== "user" || remaining <= 0) {
20317
+ continue;
20318
+ }
20319
+ const text = sanitizeText(messageText(message));
20320
+ if (!text || isCompactionSummary(text) || isPayloadHeavy(text)) {
20321
+ continue;
20322
+ }
20323
+ const tokens = estimateTextTokens(text);
20324
+ if (tokens <= remaining) {
20325
+ selected.push(text);
20326
+ remaining -= tokens;
20327
+ continue;
20328
+ }
20329
+ const truncated = truncateToTokenBudget(text, remaining);
20330
+ if (truncated) {
20331
+ selected.push(truncated);
20332
+ }
20333
+ break;
20334
+ }
20335
+ return selected.reverse().map(userMessage);
20336
+ }
20337
+ function renderMessageForSummary(message) {
20338
+ const role = message.role;
20339
+ if (typeof role !== "string") {
20340
+ return void 0;
20341
+ }
20342
+ const text = sanitizeText(messageText(message));
20343
+ if (!text) {
20344
+ return void 0;
20345
+ }
20346
+ const trimmed = text.length > MAX_RENDERED_MESSAGE_CHARS ? `${text.slice(0, MAX_RENDERED_MESSAGE_CHARS).trimEnd()}...` : text;
20347
+ return `[${role}] ${trimmed}`;
20348
+ }
20349
+ function keepTail(text, maxChars) {
20350
+ if (text.length <= maxChars) {
20351
+ return text;
20352
+ }
20353
+ const prefix = `${OMITTED_OLDER_CONTEXT_NOTICE}
20354
+ `;
20355
+ return `${prefix}${text.slice(Math.max(0, text.length - maxChars + prefix.length))}`;
20356
+ }
20357
+ function renderSummaryInput(piMessages, conversationContext) {
20358
+ const lines = [];
20359
+ const visibleContext = conversationContext?.trim();
20360
+ if (visibleContext) {
20361
+ lines.push(
20362
+ "<visible-thread-context>",
20363
+ keepTail(visibleContext, MAX_VISIBLE_CONTEXT_CHARS),
20364
+ "</visible-thread-context>",
20365
+ ""
20366
+ );
20367
+ }
20368
+ const renderedPiMessages = stripRuntimeTurnContext(piMessages).map(renderMessageForSummary).filter((line) => Boolean(line));
20369
+ if (renderedPiMessages.length > 0) {
20370
+ const piEnvelopeChars = "<pi-history>\n</pi-history>".length + 2;
20371
+ const piHistory = keepTail(
20372
+ renderedPiMessages.join("\n"),
20373
+ Math.max(
20374
+ 1,
20375
+ MAX_SUMMARY_INPUT_CHARS - lines.join("\n").length - piEnvelopeChars
20376
+ )
20377
+ );
20378
+ lines.push("<pi-history>", piHistory, "</pi-history>");
20379
+ }
20380
+ return keepTail(lines.join("\n"), MAX_SUMMARY_INPUT_CHARS);
20381
+ }
20382
+ async function summarizeContext(args, deps) {
20383
+ const source = renderSummaryInput(args.piMessages, args.conversationContext);
20384
+ const result = await deps.completeText({
20385
+ modelId: botConfig.fastModelId,
20386
+ messageAttributeMode: "metadata",
20387
+ temperature: 0,
20388
+ messages: [
20389
+ {
20390
+ role: "user",
20391
+ content: [
20392
+ "You are performing a CONTEXT CHECKPOINT COMPACTION for Junior.",
20393
+ "Create a concise handoff summary for another model that will continue this Slack thread.",
20394
+ "",
20395
+ "Include:",
20396
+ "- Current outstanding asks",
20397
+ "- Key decisions, completed work, and outcomes",
20398
+ "- Durable constraints, user preferences, IDs, URLs, artifacts, canvas links, sandbox references, and auth state",
20399
+ "- Clear next steps and unresolved blockers",
20400
+ "",
20401
+ "Do not invent details. Do not include raw secrets or credentials.",
20402
+ "",
20403
+ source
20404
+ ].join("\n"),
20405
+ timestamp: Date.now()
20406
+ }
20407
+ ],
20408
+ metadata: {
20409
+ modelId: botConfig.fastModelId,
20410
+ threadId: args.metadata?.threadId ?? "",
20411
+ channelId: args.metadata?.channelId ?? "",
20412
+ requesterId: args.metadata?.requesterId ?? "",
20413
+ runId: args.metadata?.runId ?? ""
20414
+ }
20415
+ });
20416
+ const summary = result.text.trim();
20417
+ if (!summary) {
20418
+ throw new Error("Compaction summary was empty");
20419
+ }
20420
+ return summary.slice(0, MAX_SUMMARY_CHARS);
20421
+ }
20422
+ function estimateHistoryTokens(messages) {
20423
+ const stripped = stripRuntimeTurnContext(messages);
20424
+ const usageEstimate = estimateContextTokens(stripped).tokens;
20425
+ const structuralEstimate = stripped.reduce(
20426
+ (total, message) => total + estimateTokens(message),
20427
+ 0
20428
+ );
20429
+ return Math.max(usageEstimate, structuralEstimate);
20430
+ }
20431
+ function buildReplacementHistory(args) {
20432
+ return [
20433
+ ...selectRetainedUserMessages(args.messages),
20434
+ userMessage(`${COMPACTION_SUMMARY_PREFIX}
20435
+ ${args.summary}`)
20436
+ ];
20437
+ }
20438
+ function createCompactionSessionId(previousSessionId) {
20439
+ return `compaction_${previousSessionId}`;
20440
+ }
20441
+ async function loadCompactionSource(args) {
20442
+ const checkpoint = await getAgentTurnSessionCheckpoint(
20443
+ args.conversationId,
20444
+ args.previousSessionId
20445
+ );
20446
+ if (!checkpoint) {
20447
+ return { reason: "missing_context" };
20448
+ }
20449
+ if (checkpoint.state !== "completed") {
20450
+ return { reason: "not_completed" };
20451
+ }
20452
+ const messages = checkpoint.piMessages;
20453
+ if (messages.length) {
20454
+ return {
20455
+ estimatedTokens: estimateHistoryTokens(messages),
20456
+ messages
20457
+ };
20458
+ }
20459
+ return { reason: "missing_context" };
20460
+ }
20461
+ async function maybeCompactWithDeps(args, deps) {
20462
+ const source = await loadCompactionSource({
20463
+ conversationId: args.conversationId,
20464
+ previousSessionId: args.previousSessionId
20465
+ });
20466
+ if ("reason" in source) {
20467
+ return { compacted: false, reason: source.reason };
20468
+ }
20469
+ const triggerTokens = deps.autoCompactionTriggerTokens ?? getAgentContextCompactionTriggerTokens();
20470
+ if (source.estimatedTokens <= triggerTokens) {
20471
+ return { compacted: false, reason: "below_threshold" };
20472
+ }
20473
+ args.onCompactionStart?.();
20474
+ let summary;
20475
+ try {
20476
+ summary = await summarizeContext(
20477
+ {
20478
+ conversationContext: args.conversationContext,
20479
+ piMessages: source.messages,
20480
+ metadata: args.metadata
20481
+ },
20482
+ deps
20483
+ );
20484
+ } catch (error) {
20485
+ logWarn(
20486
+ "context_compaction_summary_failed",
20487
+ {
20488
+ slackThreadId: args.metadata?.threadId,
20489
+ slackUserId: args.metadata?.requesterId,
20490
+ slackChannelId: args.metadata?.channelId,
20491
+ runId: args.metadata?.runId,
20492
+ assistantUserName: botConfig.userName,
20493
+ modelId: botConfig.fastModelId
20494
+ },
20495
+ {
20496
+ "exception.message": error instanceof Error ? error.message : String(error)
20497
+ },
20498
+ "Context compaction failed; continuing with prior history"
20499
+ );
20500
+ return { compacted: false, reason: "summary_failed" };
20501
+ }
20502
+ return await writeCompactedThreadContext(args, source.messages, summary, {
20503
+ estimatedTokens: source.estimatedTokens,
20504
+ triggerTokens
20505
+ });
20506
+ }
20507
+ async function writeCompactedThreadContext(args, sourceMessages, summary, context) {
20508
+ const replacement = buildReplacementHistory({
20509
+ messages: trimTrailingAssistantMessages(sourceMessages),
20510
+ summary
20511
+ });
20512
+ const nextSessionId = createCompactionSessionId(args.previousSessionId);
20513
+ await upsertAgentTurnSessionCheckpoint({
20514
+ conversationId: args.conversationId,
20515
+ sessionId: nextSessionId,
20516
+ sliceId: 1,
20517
+ state: "completed",
20518
+ piMessages: replacement
20519
+ });
20520
+ args.conversation.processing.lastSessionId = nextSessionId;
20521
+ updateConversationStats(args.conversation);
20522
+ setSpanAttributes({
20523
+ "app.compaction.input_messages": sourceMessages.length,
20524
+ "app.compaction.retained_messages": replacement.length - 1,
20525
+ "app.compaction.summary_chars": summary.length,
20526
+ "app.compaction.previous_session_id": args.previousSessionId,
20527
+ "app.compaction.next_session_id": nextSessionId,
20528
+ ...context.triggerTokens !== void 0 ? { "app.compaction.trigger_tokens": context.triggerTokens } : {},
20529
+ "app.context_tokens_estimated": context.estimatedTokens
20530
+ });
20531
+ return {
20532
+ compacted: true,
20533
+ piMessages: replacement,
20534
+ sessionId: nextSessionId
20535
+ };
20536
+ }
20537
+ function createContextCompactor(deps) {
20538
+ return {
20539
+ maybeCompact: async (args) => await maybeCompactWithDeps(args, deps)
20540
+ };
20541
+ }
20542
+
20181
20543
  // src/chat/slack/user.ts
20182
20544
  var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
20183
20545
  var userCache = /* @__PURE__ */ new Map();
@@ -20814,6 +21176,10 @@ function createJuniorRuntimeServices(overrides = {}) {
20814
21176
  const conversationMemory = createConversationMemoryService({
20815
21177
  completeText: overrides.conversationMemory?.completeText ?? completeText
20816
21178
  });
21179
+ const contextCompactor = createContextCompactor({
21180
+ completeText: overrides.contextCompactor?.completeText ?? completeText,
21181
+ autoCompactionTriggerTokens: overrides.contextCompactor?.autoCompactionTriggerTokens
21182
+ });
20817
21183
  const visionContext = createVisionContextService({
20818
21184
  completeText: overrides.visionContext?.completeText ?? completeText,
20819
21185
  listThreadReplies: overrides.visionContext?.listThreadReplies ?? listThreadReplies,
@@ -20821,7 +21187,9 @@ function createJuniorRuntimeServices(overrides = {}) {
20821
21187
  });
20822
21188
  return {
20823
21189
  conversationMemory,
21190
+ contextCompactor,
20824
21191
  replyExecutor: {
21192
+ contextCompactor: overrides.replyExecutor?.contextCompactor ?? contextCompactor,
20825
21193
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
20826
21194
  getAwaitingTurnContinuationRequest: overrides.replyExecutor?.getAwaitingTurnContinuationRequest ?? getAwaitingTurnContinuationRequest,
20827
21195
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
@@ -20967,7 +21335,7 @@ function buildCanvasRecoveryReply(canvasUrl) {
20967
21335
  async function loadPiMessagesForTurn(args) {
20968
21336
  const fallback = args.fallback.length > 0 ? [...args.fallback] : void 0;
20969
21337
  if (!args.conversationId) {
20970
- return fallback;
21338
+ return { piMessages: fallback };
20971
21339
  }
20972
21340
  if (args.activeTurnId) {
20973
21341
  const checkpoint2 = await getAgentTurnSessionCheckpoint(
@@ -20975,19 +21343,27 @@ async function loadPiMessagesForTurn(args) {
20975
21343
  args.activeTurnId
20976
21344
  );
20977
21345
  if (checkpoint2?.piMessages.length) {
20978
- return stripRuntimeTurnContext(
20979
- trimTrailingAssistantMessages(checkpoint2.piMessages)
20980
- );
21346
+ return {
21347
+ piMessages: stripRuntimeTurnContext(
21348
+ trimTrailingAssistantMessages(checkpoint2.piMessages)
21349
+ )
21350
+ };
20981
21351
  }
20982
21352
  }
20983
21353
  if (!args.lastSessionId) {
20984
- return fallback;
21354
+ return { piMessages: fallback };
20985
21355
  }
20986
21356
  const checkpoint = await getAgentTurnSessionCheckpoint(
20987
21357
  args.conversationId,
20988
21358
  args.lastSessionId
20989
21359
  );
20990
- return checkpoint?.state === "completed" && checkpoint.piMessages.length > 0 ? stripRuntimeTurnContext(checkpoint.piMessages) : fallback;
21360
+ if (checkpoint?.state === "completed" && checkpoint.piMessages.length > 0) {
21361
+ return {
21362
+ compactionSessionId: args.lastSessionId,
21363
+ piMessages: stripRuntimeTurnContext(checkpoint.piMessages)
21364
+ };
21365
+ }
21366
+ return { piMessages: fallback };
20991
21367
  }
20992
21368
  function createReplyToThread(deps) {
20993
21369
  return async function replyToThread(thread, message, options = {}) {
@@ -21218,17 +21594,14 @@ function createReplyToThread(deps) {
21218
21594
  }
21219
21595
  );
21220
21596
  const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
21221
- const piMessages = await loadPiMessagesForTurn({
21222
- conversationId,
21223
- activeTurnId,
21224
- lastSessionId: lastSessionIdForHistory,
21225
- fallback: preparedState.conversation.piMessages
21226
- });
21227
21597
  const status = createSlackAdapterAssistantStatusSession({
21228
21598
  channelId: assistantThreadContext?.channelId,
21229
21599
  threadTs: assistantThreadContext?.threadTs,
21230
21600
  getSlackAdapter: deps.getSlackAdapter
21231
21601
  });
21602
+ const compactingStatus = {
21603
+ text: "Compacting context"
21604
+ };
21232
21605
  const postThreadReply = async (payload, stage) => {
21233
21606
  await beforeFirstResponsePost();
21234
21607
  try {
@@ -21248,24 +21621,52 @@ function createReplyToThread(deps) {
21248
21621
  throw error;
21249
21622
  }
21250
21623
  };
21251
- status.start();
21252
- const assistantTitleTask = maybeUpdateAssistantTitle({
21253
- assistantThreadContext,
21254
- assistantUserName: botConfig.userName,
21255
- artifacts: preparedState.artifacts,
21256
- channelId,
21257
- conversation: preparedState.conversation,
21258
- generateThreadTitle: deps.services.generateThreadTitle,
21259
- getSlackAdapter: deps.getSlackAdapter,
21260
- modelId: botConfig.fastModelId,
21261
- requesterId: message.author.userId,
21262
- runId,
21263
- threadId
21264
- });
21265
21624
  let persistedAtLeastOnce = false;
21266
21625
  let shouldPersistFailureState = true;
21267
21626
  let latestArtifacts = preparedState.artifacts;
21268
21627
  try {
21628
+ const loadedPiMessages = await loadPiMessagesForTurn({
21629
+ conversationId,
21630
+ activeTurnId,
21631
+ lastSessionId: lastSessionIdForHistory,
21632
+ fallback: preparedState.conversation.piMessages
21633
+ });
21634
+ let piMessages = loadedPiMessages.piMessages;
21635
+ if (conversationId && loadedPiMessages.compactionSessionId && piMessages?.length) {
21636
+ const compaction = await deps.services.contextCompactor.maybeCompact({
21637
+ conversation: preparedState.conversation,
21638
+ conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
21639
+ conversationId,
21640
+ metadata: {
21641
+ threadId,
21642
+ requesterId: message.author.userId,
21643
+ channelId,
21644
+ runId
21645
+ },
21646
+ onCompactionStart: () => status.start(compactingStatus),
21647
+ previousSessionId: loadedPiMessages.compactionSessionId
21648
+ });
21649
+ if (compaction.compacted) {
21650
+ piMessages = compaction.piMessages;
21651
+ await persistThreadState(thread, {
21652
+ conversation: preparedState.conversation
21653
+ });
21654
+ }
21655
+ }
21656
+ status.start();
21657
+ const assistantTitleTask = maybeUpdateAssistantTitle({
21658
+ assistantThreadContext,
21659
+ assistantUserName: botConfig.userName,
21660
+ artifacts: preparedState.artifacts,
21661
+ channelId,
21662
+ conversation: preparedState.conversation,
21663
+ generateThreadTitle: deps.services.generateThreadTitle,
21664
+ getSlackAdapter: deps.getSlackAdapter,
21665
+ modelId: botConfig.fastModelId,
21666
+ requesterId: message.author.userId,
21667
+ runId,
21668
+ threadId
21669
+ });
21269
21670
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
21270
21671
  let reply = await deps.services.generateAssistantReply(userText, {
21271
21672
  requester: {
@@ -1,15 +1,18 @@
1
1
  import { type ConversationMemoryDeps, type ConversationMemoryService } from "@/chat/services/conversation-memory";
2
+ import { type ContextCompactor, type ContextCompactorDeps } from "@/chat/services/context-compaction";
2
3
  import { type SubscribedReplyPolicy, type SubscribedReplyPolicyDeps } from "@/chat/services/subscribed-reply-policy";
3
4
  import type { ReplyExecutorServices } from "@/chat/runtime/reply-executor";
4
5
  import { type VisionContextDeps, type VisionContextService } from "@/chat/services/vision-context";
5
6
  export interface JuniorRuntimeServices {
6
7
  conversationMemory: ConversationMemoryService;
8
+ contextCompactor: ContextCompactor;
7
9
  replyExecutor: ReplyExecutorServices;
8
10
  subscribedReplyPolicy: SubscribedReplyPolicy;
9
11
  visionContext: VisionContextService;
10
12
  }
11
13
  export interface JuniorRuntimeServiceOverrides {
12
14
  conversationMemory?: Partial<ConversationMemoryDeps>;
15
+ contextCompactor?: Partial<ContextCompactorDeps>;
13
16
  replyExecutor?: Partial<Omit<ReplyExecutorServices, "generateThreadTitle">>;
14
17
  subscribedReplyPolicy?: Partial<SubscribedReplyPolicyDeps>;
15
18
  visionContext?: Partial<VisionContextDeps>;
@@ -5,6 +5,7 @@ export interface BotConfig {
5
5
  fastModelId: string;
6
6
  loadingMessages: string[];
7
7
  modelId: string;
8
+ modelContextWindowTokens?: number;
8
9
  visionModelId?: string;
9
10
  turnTimeoutMs: number;
10
11
  userName: string;
@@ -23,6 +23,7 @@ export declare function completeText(params: {
23
23
  modelId: string;
24
24
  system?: string;
25
25
  messages: Message[];
26
+ messageAttributeMode?: "content" | "metadata";
26
27
  thinkingLevel?: ThinkingLevel;
27
28
  temperature?: number;
28
29
  maxTokens?: number;
@@ -3,9 +3,11 @@ import type { SlackAdapter } from "@chat-adapter/slack";
3
3
  import { generateAssistantReply as generateAssistantReplyImpl } from "@/chat/respond";
4
4
  import type { PreparedTurnState } from "@/chat/runtime/turn-preparation";
5
5
  import { type ConversationMemoryService } from "@/chat/services/conversation-memory";
6
+ import type { ContextCompactor } from "@/chat/services/context-compaction";
6
7
  import { lookupSlackUser } from "@/chat/slack/user";
7
8
  import type { TurnContinuationRequest } from "@/chat/services/timeout-resume";
8
9
  export interface ReplyExecutorServices {
10
+ contextCompactor: ContextCompactor;
9
11
  generateAssistantReply: typeof generateAssistantReplyImpl;
10
12
  generateThreadTitle: ConversationMemoryService["generateThreadTitle"];
11
13
  getAwaitingTurnContinuationRequest: (args: {
@@ -0,0 +1,14 @@
1
+ export interface ModelContextBudget {
2
+ contextWindow: number;
3
+ maxTokens: number;
4
+ }
5
+ /** Estimate text tokens with the shared coarse heuristic used for local budgets. */
6
+ export declare function estimateTextTokens(text: string): number;
7
+ /** Derive the automatic compaction threshold from model context capacity. */
8
+ export declare function calculateContextCompactionTriggerTokens(model: ModelContextBudget): number;
9
+ /** Derive the post-compaction target from the automatic trigger threshold. */
10
+ export declare function calculateContextCompactionTargetTokens(triggerTokens: number): number;
11
+ /** Resolve the automatic compaction threshold for the active agent model. */
12
+ export declare function getAgentContextCompactionTriggerTokens(): number;
13
+ /** Resolve the visible conversation compaction threshold for the auxiliary model. */
14
+ export declare function getConversationContextCompactionTriggerTokens(): number;
@@ -0,0 +1,33 @@
1
+ import type { completeText } from "@/chat/pi/client";
2
+ import type { PiMessage } from "@/chat/pi/messages";
3
+ import type { ThreadConversationState } from "@/chat/state/conversation";
4
+ export interface ContextCompactorDeps {
5
+ completeText: typeof completeText;
6
+ autoCompactionTriggerTokens?: number;
7
+ }
8
+ export interface ContextCompactor {
9
+ maybeCompact: (args: CompactContextArgs) => Promise<CompactContextResult>;
10
+ }
11
+ export interface CompactContextArgs {
12
+ conversation: ThreadConversationState;
13
+ conversationContext?: string;
14
+ conversationId: string;
15
+ onCompactionStart?: () => void;
16
+ previousSessionId: string;
17
+ metadata?: {
18
+ channelId?: string;
19
+ requesterId?: string;
20
+ runId?: string;
21
+ threadId?: string;
22
+ };
23
+ }
24
+ export interface CompactContextResult {
25
+ compacted: boolean;
26
+ piMessages?: PiMessage[];
27
+ reason?: "below_threshold" | "missing_context" | "not_completed" | "summary_failed";
28
+ sessionId?: string;
29
+ }
30
+ /** Build retained user messages for a compacted Pi replacement history. */
31
+ export declare function selectRetainedUserMessages(messages: PiMessage[], maxTokens?: number): PiMessage[];
32
+ /** Build the service that owns local context compaction and checkpoint forks. */
33
+ export declare function createContextCompactor(deps: ContextCompactorDeps): ContextCompactor;
@@ -1,7 +1,7 @@
1
1
  import { type AssistantStatusSpec } from "@/chat/slack/assistant-thread/status-render";
2
2
  export type TimerHandle = ReturnType<typeof setTimeout>;
3
3
  export interface AssistantStatusSession {
4
- start: () => void;
4
+ start: (status?: AssistantStatusSpec) => void;
5
5
  stop: () => Promise<void>;
6
6
  update: (status: AssistantStatusSpec) => void;
7
7
  }
@@ -54,6 +54,35 @@ function getPiGatewayApiKeyOverride() {
54
54
  function extractText(message) {
55
55
  return (message.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text ?? "").join("").trim();
56
56
  }
57
+ function contentMetadata(content) {
58
+ if (typeof content === "string") {
59
+ return [{ type: "text", chars: content.length }];
60
+ }
61
+ if (!Array.isArray(content)) {
62
+ return { type: typeof content };
63
+ }
64
+ return content.map((part) => {
65
+ if (!part || typeof part !== "object") {
66
+ return { type: typeof part };
67
+ }
68
+ const record = part;
69
+ const type = typeof record.type === "string" ? record.type : "unknown";
70
+ return {
71
+ type,
72
+ ...typeof record.text === "string" ? { chars: record.text.length } : {},
73
+ ...typeof record.mimeType === "string" ? { mimeType: record.mimeType } : {},
74
+ ...typeof record.mediaType === "string" ? { mediaType: record.mediaType } : {},
75
+ ...typeof record.data === "string" ? { dataChars: record.data.length } : {}
76
+ };
77
+ });
78
+ }
79
+ function toMessageMetadata(message) {
80
+ const record = message;
81
+ return {
82
+ role: record.role,
83
+ content: contentMetadata(record.content)
84
+ };
85
+ }
57
86
  function parseJsonCandidate(text) {
58
87
  const trimmed = text.trim();
59
88
  if (!trimmed) return void 0;
@@ -126,8 +155,13 @@ function resolveGatewayModel(modelId) {
126
155
  async function completeText(params) {
127
156
  const model = resolveGatewayModel(params.modelId);
128
157
  const apiKey = getPiGatewayApiKeyOverride();
129
- const requestMessagesAttribute = serializeGenAiAttribute(params.messages);
130
- const systemInstructionsAttribute = params.system ? serializeGenAiAttribute([{ type: "text", content: params.system }]) : void 0;
158
+ const messageAttributeMode = params.messageAttributeMode ?? "content";
159
+ const requestMessagesAttribute = serializeGenAiAttribute(
160
+ messageAttributeMode === "metadata" ? params.messages.map(toMessageMetadata) : params.messages
161
+ );
162
+ const systemInstructionsAttribute = params.system ? serializeGenAiAttribute(
163
+ messageAttributeMode === "metadata" ? [{ type: "text", chars: params.system.length }] : [{ type: "text", content: params.system }]
164
+ ) : void 0;
131
165
  const baseAttributes = {
132
166
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
133
167
  "gen_ai.operation.name": GEN_AI_OPERATION_CHAT,
@@ -161,12 +195,19 @@ async function completeText(params) {
161
195
  }
162
196
  );
163
197
  const outputText = extractText(message);
164
- const outputMessagesAttribute = serializeGenAiAttribute([
165
- {
166
- role: "assistant",
167
- content: outputText ? [{ type: "text", text: outputText }] : []
168
- }
169
- ]);
198
+ const outputMessagesAttribute = serializeGenAiAttribute(
199
+ messageAttributeMode === "metadata" ? [
200
+ {
201
+ role: "assistant",
202
+ content: outputText ? [{ type: "text", chars: outputText.length }] : []
203
+ }
204
+ ] : [
205
+ {
206
+ role: "assistant",
207
+ content: outputText ? [{ type: "text", text: outputText }] : []
208
+ }
209
+ ]
210
+ );
170
211
  const usageAttributes = extractGenAiUsageAttributes(message);
171
212
  const endAttributes = {
172
213
  ...baseAttributes,
@@ -336,6 +377,17 @@ function parseAdvisorThinkingLevel(rawValue) {
336
377
  `AI_ADVISOR_THINKING_LEVEL must be one of: minimal, low, medium, high, xhigh`
337
378
  );
338
379
  }
380
+ function parseOptionalPositiveInteger(envName, rawValue) {
381
+ const trimmed = toOptionalTrimmed(rawValue);
382
+ if (trimmed === void 0) {
383
+ return void 0;
384
+ }
385
+ const value = Number.parseInt(trimmed, 10);
386
+ if (!Number.isSafeInteger(value) || value <= 0 || String(value) !== trimmed) {
387
+ throw new Error(`${envName} must be a positive integer`);
388
+ }
389
+ return value;
390
+ }
339
391
  var DEFAULT_MODEL_ID = getModel("vercel-ai-gateway", "openai/gpt-5.4").id;
340
392
  var DEFAULT_FAST_MODEL_ID = getModel(
341
393
  "vercel-ai-gateway",
@@ -363,6 +415,10 @@ function readBotConfig(env) {
363
415
  return {
364
416
  userName: env.JUNIOR_BOT_NAME ?? "junior",
365
417
  modelId: validateGatewayModelId(env.AI_MODEL) ?? DEFAULT_MODEL_ID,
418
+ modelContextWindowTokens: parseOptionalPositiveInteger(
419
+ "AI_MODEL_CONTEXT_WINDOW_TOKENS",
420
+ env.AI_MODEL_CONTEXT_WINDOW_TOKENS
421
+ ),
366
422
  fastModelId: validateGatewayModelId(env.AI_FAST_MODEL ?? env.AI_MODEL) ?? DEFAULT_FAST_MODEL_ID,
367
423
  loadingMessages: parseLoadingMessages(env.JUNIOR_LOADING_MESSAGES),
368
424
  visionModelId: validateGatewayModelId(env.AI_VISION_MODEL),
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  disconnectStateAdapter,
3
3
  resolveRuntimeDependencySnapshot
4
- } from "../chunk-XI3CFWTA.js";
4
+ } from "../chunk-AA5TIFN5.js";
5
5
  import {
6
6
  getPluginProviders,
7
7
  getPluginRuntimeDependencies,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -46,7 +46,7 @@
46
46
  "node-html-markdown": "^2.0.0",
47
47
  "yaml": "^2.9.0",
48
48
  "zod": "^4.4.3",
49
- "@sentry/junior-plugin-api": "0.55.0"
49
+ "@sentry/junior-plugin-api": "0.56.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@sentry/node": ">=10.0.0"