@sentry/junior 0.51.0 → 0.53.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 (2) hide show
  1. package/dist/app.js +523 -295
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -494,6 +494,7 @@ function coerceThreadConversationState(value) {
494
494
  const processing = {
495
495
  activeTurnId: toOptionalString(rawProcessing.activeTurnId),
496
496
  lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
497
+ lastSessionId: toOptionalString(rawProcessing.lastSessionId),
497
498
  pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
498
499
  };
499
500
  const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
@@ -2101,17 +2102,24 @@ function startActiveTurn(args) {
2101
2102
  args.conversation.processing.activeTurnId = args.nextTurnId;
2102
2103
  args.updateConversationStats(args.conversation);
2103
2104
  }
2104
- function markTurnCompleted(args) {
2105
- if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2106
- args.conversation.processing.activeTurnId = void 0;
2105
+ function clearActiveTurn(conversation, sessionId) {
2106
+ if (!sessionId || conversation.processing.activeTurnId === sessionId) {
2107
+ conversation.processing.activeTurnId = void 0;
2107
2108
  }
2109
+ }
2110
+ function markTurnClosed(args) {
2111
+ clearActiveTurn(args.conversation, args.sessionId);
2112
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2113
+ args.updateConversationStats(args.conversation);
2114
+ }
2115
+ function markTurnCompleted(args) {
2116
+ clearActiveTurn(args.conversation, args.sessionId);
2117
+ args.conversation.processing.lastSessionId = args.sessionId;
2108
2118
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2109
2119
  args.updateConversationStats(args.conversation);
2110
2120
  }
2111
2121
  function markTurnFailed(args) {
2112
- if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2113
- args.conversation.processing.activeTurnId = void 0;
2114
- }
2122
+ clearActiveTurn(args.conversation, args.sessionId);
2115
2123
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2116
2124
  args.markConversationMessage(args.conversation, args.userMessageId, {
2117
2125
  replied: false,
@@ -2450,6 +2458,9 @@ import { Agent as Agent2 } from "@mariozechner/pi-agent-core";
2450
2458
  import fs from "fs";
2451
2459
  import path2 from "path";
2452
2460
 
2461
+ // src/chat/turn-context-tag.ts
2462
+ var TURN_CONTEXT_TAG = "runtime-turn-context";
2463
+
2453
2464
  // src/chat/interruption-marker.ts
2454
2465
  var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2455
2466
  function getInterruptionMarker() {
@@ -3020,7 +3031,6 @@ function formatSlackCapabilityNames(capabilities) {
3020
3031
  }
3021
3032
  var HEADER = "You are a Slack-based helper assistant. Follow the personality block for voice and tone in every reply. The behavior and output blocks define platform mechanics and override personality only when those mechanics conflict.";
3022
3033
  var TURN_CONTEXT_HEADER = "Per-turn runtime context for this request. Treat these blocks as trusted runtime facts and skill/provider instructions for the current turn; the static system prompt remains authoritative.";
3023
- var TURN_CONTEXT_TAG = "runtime-turn-context";
3024
3034
  var TOOL_POLICY_RULES = [
3025
3035
  "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
3026
3036
  "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
@@ -7882,6 +7892,7 @@ import { Type as Type21 } from "@sinclair/typebox";
7882
7892
 
7883
7893
  // src/chat/respond-helpers.ts
7884
7894
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
7895
+ var RUNTIME_TURN_CONTEXT_START = `<${TURN_CONTEXT_TAG}>`;
7885
7896
  function getSessionIdentifiers(context) {
7886
7897
  return {
7887
7898
  conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
@@ -8044,6 +8055,75 @@ function getPiMessageRole(value) {
8044
8055
  const role = value.role;
8045
8056
  return typeof role === "string" ? role : void 0;
8046
8057
  }
8058
+ function getUserMessageContent(message) {
8059
+ const record = message;
8060
+ return record.role === "user" && Array.isArray(record.content) ? record.content : void 0;
8061
+ }
8062
+ function isRuntimeTurnContextPart(part, marker) {
8063
+ return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.startsWith(marker);
8064
+ }
8065
+ function replaceRuntimeTurnContext(message, turnContextPrompt) {
8066
+ const content = getUserMessageContent(message);
8067
+ if (!content) {
8068
+ return void 0;
8069
+ }
8070
+ const marker = turnContextPrompt.split("\n", 1)[0];
8071
+ const contextIndex = content.findIndex(
8072
+ (part) => isRuntimeTurnContextPart(part, marker)
8073
+ );
8074
+ if (contextIndex < 0) {
8075
+ return void 0;
8076
+ }
8077
+ const nextContent = [...content];
8078
+ nextContent[contextIndex] = {
8079
+ ...nextContent[contextIndex],
8080
+ text: turnContextPrompt
8081
+ };
8082
+ return {
8083
+ ...message,
8084
+ content: nextContent
8085
+ };
8086
+ }
8087
+ function refreshRuntimeTurnContext(messages, turnContextPrompt) {
8088
+ for (let index = 0; index < messages.length; index += 1) {
8089
+ const updated = replaceRuntimeTurnContext(
8090
+ messages[index],
8091
+ turnContextPrompt
8092
+ );
8093
+ if (!updated) {
8094
+ continue;
8095
+ }
8096
+ const nextMessages = [...messages];
8097
+ nextMessages[index] = updated;
8098
+ return nextMessages;
8099
+ }
8100
+ return [
8101
+ ...messages,
8102
+ {
8103
+ role: "user",
8104
+ content: [{ type: "text", text: turnContextPrompt }],
8105
+ timestamp: Date.now()
8106
+ }
8107
+ ];
8108
+ }
8109
+ function stripRuntimeTurnContext(messages) {
8110
+ return messages.flatMap((message) => {
8111
+ const content = getUserMessageContent(message);
8112
+ if (!content) {
8113
+ return [message];
8114
+ }
8115
+ const nextContent = content.filter(
8116
+ (part) => !isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
8117
+ );
8118
+ if (nextContent.length === content.length) {
8119
+ return [message];
8120
+ }
8121
+ if (nextContent.length === 0) {
8122
+ return [];
8123
+ }
8124
+ return [{ ...message, content: nextContent }];
8125
+ });
8126
+ }
8047
8127
  function extractAssistantText(message) {
8048
8128
  const content = message.content ?? [];
8049
8129
  return content.filter(
@@ -9138,84 +9218,51 @@ function createTracedStreamFn(base = streamSimple) {
9138
9218
  // src/chat/sandbox/sandbox.ts
9139
9219
  import fs4 from "fs/promises";
9140
9220
 
9141
- // src/chat/sandbox/egress-policy.ts
9142
- function matchesSandboxEgressDomain(host, domain) {
9143
- return host.toLowerCase() === domain.toLowerCase();
9144
- }
9145
- function manifestDomains(manifest) {
9146
- const domains = /* @__PURE__ */ new Set([
9147
- ...manifest.credentials?.domains ?? [],
9148
- ...manifest.domains ?? []
9149
- ]);
9150
- return [...domains].sort((left, right) => left.localeCompare(right));
9151
- }
9152
- function providerEntries() {
9153
- return getPluginProviders().map((plugin) => ({
9154
- provider: plugin.manifest.name,
9155
- domains: manifestDomains(plugin.manifest)
9156
- })).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
9157
- }
9158
- function resolveSandboxEgressProviderForHost(host) {
9159
- return providerEntries().find(
9160
- (entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
9161
- )?.provider;
9162
- }
9163
- function sandboxProxyUrl() {
9164
- const baseUrl = resolveBaseUrl();
9165
- if (!baseUrl) {
9166
- throw new Error(
9167
- "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
9168
- );
9169
- }
9170
- return new URL("/", baseUrl).toString();
9221
+ // src/chat/sandbox/egress-session.ts
9222
+ import { createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
9223
+ var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
9224
+ var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
9225
+ var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
9226
+ var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
9227
+ function leaseKey(provider, context) {
9228
+ return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${context.requesterId}:${context.egressId}:${context.contextId}`;
9171
9229
  }
9172
- function buildSandboxEgressNetworkPolicy() {
9173
- const allow = {
9174
- "*": []
9175
- };
9176
- const entries = providerEntries();
9177
- if (entries.length === 0) {
9178
- return { allow };
9230
+ function getSandboxEgressSecret() {
9231
+ const explicit = process.env.JUNIOR_SANDBOX_EGRESS_SECRET?.trim();
9232
+ if (explicit) {
9233
+ return explicit;
9179
9234
  }
9180
- const forwardURL = sandboxProxyUrl();
9181
- for (const entry of entries) {
9182
- for (const domain of entry.domains) {
9183
- allow[domain] = [{ forwardURL }];
9184
- }
9235
+ const sharedInternal = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
9236
+ if (sharedInternal) {
9237
+ return sharedInternal;
9185
9238
  }
9186
- return { allow };
9239
+ throw new Error(
9240
+ "Cannot determine sandbox egress secret (set JUNIOR_SANDBOX_EGRESS_SECRET or JUNIOR_INTERNAL_RESUME_SECRET)"
9241
+ );
9187
9242
  }
9188
- async function resolveSandboxCommandEnvironment() {
9189
- const env = {};
9190
- for (const plugin of getPluginProviders().sort(
9191
- (left, right) => left.manifest.name.localeCompare(right.manifest.name)
9192
- )) {
9193
- Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
9194
- const credentials = plugin.manifest.credentials;
9195
- if (credentials) {
9196
- env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
9197
- }
9198
- }
9199
- return env;
9243
+ function base64Url(input) {
9244
+ return Buffer.from(input, "utf8").toString("base64url");
9200
9245
  }
9201
-
9202
- // src/chat/sandbox/egress-session.ts
9203
- import { randomUUID as randomUUID3 } from "crypto";
9204
- var SANDBOX_EGRESS_SESSION_PREFIX = "sandbox-egress-session";
9205
- var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
9206
- var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
9207
- function sessionKey2(egressId) {
9208
- return `${SANDBOX_EGRESS_SESSION_PREFIX}:${egressId}`;
9246
+ function fromBase64Url(input) {
9247
+ return Buffer.from(input, "base64url").toString("utf8");
9209
9248
  }
9210
- function leaseKey(egressId, provider, session) {
9211
- return `${SANDBOX_EGRESS_LEASE_PREFIX}:${egressId}:${provider}:${session.requesterId}:${session.activationId}`;
9249
+ function signPayload(payload) {
9250
+ return createHmac("sha256", getSandboxEgressSecret()).update(payload).digest("base64url");
9212
9251
  }
9213
- function parseSession(value) {
9252
+ function timingSafeMatch(expected, actual) {
9253
+ const expectedBuffer = Buffer.from(expected);
9254
+ const actualBuffer = Buffer.from(actual);
9255
+ if (expectedBuffer.length !== actualBuffer.length) {
9256
+ return false;
9257
+ }
9258
+ return timingSafeEqual(expectedBuffer, actualBuffer);
9259
+ }
9260
+ function parseRequesterContext(value) {
9214
9261
  if (!value || typeof value !== "object") {
9215
9262
  return void 0;
9216
9263
  }
9217
9264
  const record = value;
9218
- if (typeof record.requesterId !== "string" || typeof record.expiresAtMs !== "number" || !Number.isFinite(record.expiresAtMs) || typeof record.activationId !== "string" || !record.activationId) {
9265
+ if (typeof record.requesterId !== "string" || !record.requesterId || typeof record.egressId !== "string" || !record.egressId || typeof record.expiresAtMs !== "number" || !Number.isFinite(record.expiresAtMs) || typeof record.contextId !== "string" || !record.contextId) {
9219
9266
  return void 0;
9220
9267
  }
9221
9268
  if (record.expiresAtMs <= Date.now()) {
@@ -9223,8 +9270,9 @@ function parseSession(value) {
9223
9270
  }
9224
9271
  return {
9225
9272
  requesterId: record.requesterId,
9273
+ egressId: record.egressId,
9226
9274
  expiresAtMs: record.expiresAtMs,
9227
- activationId: record.activationId
9275
+ contextId: record.contextId
9228
9276
  };
9229
9277
  }
9230
9278
  function parseLease(value) {
@@ -9253,50 +9301,127 @@ function parseLease(value) {
9253
9301
  headerTransforms
9254
9302
  };
9255
9303
  }
9256
- async function upsertSandboxEgressSession(input) {
9257
- const state = getStateAdapter();
9258
- await state.connect();
9304
+ function createSandboxEgressRequesterToken(input) {
9259
9305
  const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
9260
9306
  const now = Date.now();
9261
- const session = {
9307
+ const context = {
9262
9308
  requesterId: input.requesterId,
9309
+ egressId: input.egressId,
9263
9310
  expiresAtMs: now + ttlMs,
9264
- activationId: randomUUID3()
9311
+ contextId: randomUUID3()
9265
9312
  };
9266
- await state.set(sessionKey2(input.egressId), session, ttlMs);
9267
- }
9268
- async function clearSandboxEgressSession(egressId) {
9269
- const state = getStateAdapter();
9270
- await state.connect();
9271
- await state.delete(sessionKey2(egressId));
9313
+ const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(
9314
+ JSON.stringify(context)
9315
+ )}`;
9316
+ return `${payload}.${signPayload(payload)}`;
9272
9317
  }
9273
- async function getSandboxEgressSession(egressId) {
9274
- const state = getStateAdapter();
9275
- await state.connect();
9276
- return parseSession(await state.get(sessionKey2(egressId)));
9318
+ function parseSandboxEgressRequesterToken(token) {
9319
+ if (!token) {
9320
+ return void 0;
9321
+ }
9322
+ const parts = token.split(".");
9323
+ if (parts.length !== 3 || parts[0] !== SANDBOX_EGRESS_TOKEN_VERSION) {
9324
+ return void 0;
9325
+ }
9326
+ const encodedSession = parts[1];
9327
+ const signature = parts[2];
9328
+ if (!encodedSession || !signature) {
9329
+ return void 0;
9330
+ }
9331
+ const payload = `${parts[0]}.${encodedSession}`;
9332
+ if (!timingSafeMatch(signPayload(payload), signature)) {
9333
+ return void 0;
9334
+ }
9335
+ try {
9336
+ return parseRequesterContext(JSON.parse(fromBase64Url(encodedSession)));
9337
+ } catch {
9338
+ return void 0;
9339
+ }
9277
9340
  }
9278
- async function setSandboxEgressCredentialLease(egressId, session, lease) {
9341
+ async function setSandboxEgressCredentialLease(context, lease) {
9279
9342
  const leaseExpiresAtMs = Date.parse(lease.expiresAt);
9280
9343
  if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
9281
9344
  return;
9282
9345
  }
9283
9346
  const ttlMs = Math.max(
9284
9347
  1,
9285
- Math.min(leaseExpiresAtMs, session.expiresAtMs) - Date.now()
9348
+ Math.min(leaseExpiresAtMs, context.expiresAtMs) - Date.now()
9286
9349
  );
9287
9350
  const state = getStateAdapter();
9288
9351
  await state.connect();
9289
- await state.set(leaseKey(egressId, lease.provider, session), lease, ttlMs);
9352
+ await state.set(leaseKey(lease.provider, context), lease, ttlMs);
9290
9353
  }
9291
- async function getSandboxEgressCredentialLease(egressId, provider, session) {
9354
+ async function getSandboxEgressCredentialLease(provider, context) {
9292
9355
  const state = getStateAdapter();
9293
9356
  await state.connect();
9294
- return parseLease(await state.get(leaseKey(egressId, provider, session)));
9357
+ return parseLease(await state.get(leaseKey(provider, context)));
9295
9358
  }
9296
- async function clearSandboxEgressCredentialLease(egressId, provider, session) {
9359
+ async function clearSandboxEgressCredentialLease(provider, context) {
9297
9360
  const state = getStateAdapter();
9298
9361
  await state.connect();
9299
- await state.delete(leaseKey(egressId, provider, session));
9362
+ await state.delete(leaseKey(provider, context));
9363
+ }
9364
+
9365
+ // src/chat/sandbox/egress-policy.ts
9366
+ function matchesSandboxEgressDomain(host, domain) {
9367
+ return host.toLowerCase() === domain.toLowerCase();
9368
+ }
9369
+ function manifestDomains(manifest) {
9370
+ const domains = /* @__PURE__ */ new Set([
9371
+ ...manifest.credentials?.domains ?? [],
9372
+ ...manifest.domains ?? []
9373
+ ]);
9374
+ return [...domains].sort((left, right) => left.localeCompare(right));
9375
+ }
9376
+ function providerEntries() {
9377
+ return getPluginProviders().map((plugin) => ({
9378
+ provider: plugin.manifest.name,
9379
+ domains: manifestDomains(plugin.manifest)
9380
+ })).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
9381
+ }
9382
+ function resolveSandboxEgressProviderForHost(host) {
9383
+ return providerEntries().find(
9384
+ (entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
9385
+ )?.provider;
9386
+ }
9387
+ function sandboxProxyUrl(requesterToken) {
9388
+ const baseUrl = resolveBaseUrl();
9389
+ if (!baseUrl) {
9390
+ throw new Error(
9391
+ "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
9392
+ );
9393
+ }
9394
+ const path11 = requesterToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${requesterToken}` : SANDBOX_EGRESS_PROXY_PATH;
9395
+ return new URL(path11, baseUrl).toString();
9396
+ }
9397
+ function buildSandboxEgressNetworkPolicy(input) {
9398
+ const allow = {
9399
+ "*": []
9400
+ };
9401
+ const entries = providerEntries();
9402
+ if (entries.length === 0) {
9403
+ return { allow };
9404
+ }
9405
+ const forwardURL = sandboxProxyUrl(input?.requesterToken);
9406
+ for (const entry of entries) {
9407
+ for (const domain of entry.domains) {
9408
+ allow[domain] = [{ forwardURL }];
9409
+ }
9410
+ }
9411
+ return { allow };
9412
+ }
9413
+ async function resolveSandboxCommandEnvironment() {
9414
+ const env = {};
9415
+ for (const plugin of getPluginProviders().sort(
9416
+ (left, right) => left.manifest.name.localeCompare(right.manifest.name)
9417
+ )) {
9418
+ Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
9419
+ const credentials = plugin.manifest.credentials;
9420
+ if (credentials) {
9421
+ env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
9422
+ }
9423
+ }
9424
+ return env;
9300
9425
  }
9301
9426
 
9302
9427
  // src/chat/sandbox/http-error-details.ts
@@ -10601,7 +10726,6 @@ function createSandboxSessionManager(options) {
10601
10726
  }
10602
10727
  };
10603
10728
  const buildToolExecutors = async (sandboxInstance) => {
10604
- const activeSandboxId = sandboxInstance.sandboxId;
10605
10729
  const toolkit = await withSandboxSpan(
10606
10730
  "sandbox.bash_tool.init",
10607
10731
  "sandbox.tool.init",
@@ -10621,35 +10745,9 @@ function createSandboxSessionManager(options) {
10621
10745
  }
10622
10746
  return {
10623
10747
  bash: async (input) => {
10624
- const commandEgressId = sandboxInstance.sandboxEgressId;
10625
10748
  let timedOut = false;
10626
10749
  let timeoutId;
10627
- let commandFinished = false;
10628
- const finishCommand = async () => {
10629
- if (commandFinished) {
10630
- return;
10631
- }
10632
- commandFinished = true;
10633
- await options?.afterCommand?.(commandEgressId);
10634
- await refreshNetworkPolicy(sandboxInstance);
10635
- };
10636
- const finishCommandBestEffort = async () => {
10637
- try {
10638
- await finishCommand();
10639
- } catch (error) {
10640
- logWarn(
10641
- "sandbox_command_cleanup_failed",
10642
- traceContext,
10643
- {
10644
- "app.sandbox.id": activeSandboxId,
10645
- "error.type": error instanceof Error ? error.name : "sandbox_command_cleanup_error"
10646
- },
10647
- "Sandbox command cleanup failed"
10648
- );
10649
- }
10650
- };
10651
10750
  try {
10652
- await options?.beforeCommand?.(commandEgressId);
10653
10751
  await refreshNetworkPolicy(sandboxInstance);
10654
10752
  const sandboxCommandEnv = await resolveCommandEnv();
10655
10753
  const script = buildNonInteractiveShellScript(input.command, {
@@ -10671,7 +10769,6 @@ function createSandboxSessionManager(options) {
10671
10769
  cwd: SANDBOX_WORKSPACE_ROOT,
10672
10770
  ...controller ? { signal: controller.signal } : {}
10673
10771
  });
10674
- await finishCommandBestEffort();
10675
10772
  return await readCommandOutput(commandResult2);
10676
10773
  } catch (error) {
10677
10774
  if (timedOut) {
@@ -10692,7 +10789,6 @@ function createSandboxSessionManager(options) {
10692
10789
  if (timeoutId) {
10693
10790
  clearTimeout(timeoutId);
10694
10791
  }
10695
- await finishCommandBestEffort();
10696
10792
  }
10697
10793
  },
10698
10794
  readFile: async (input) => await executeReadFile(input, {
@@ -10781,25 +10877,40 @@ function createSandboxExecutor(options) {
10781
10877
  let referenceFiles = [];
10782
10878
  const traceContext = options?.traceContext ?? {};
10783
10879
  const credentialEgress = options?.credentialEgress;
10784
- const authorizeSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10785
- await upsertSandboxEgressSession({
10786
- egressId,
10880
+ const sandboxEgressTokenTtlMs = Math.max(
10881
+ 1,
10882
+ options?.timeoutMs ?? 1e3 * 60 * 30
10883
+ );
10884
+ const sandboxEgressRequesterTokens = /* @__PURE__ */ new Map();
10885
+ const sandboxEgressRequesterTokenFor = (egressId) => {
10886
+ const cached = sandboxEgressRequesterTokens.get(egressId);
10887
+ if (cached && cached.expiresAtMs > Date.now()) {
10888
+ return cached.token;
10889
+ }
10890
+ if (!credentialEgress) {
10891
+ throw new Error("Sandbox credential egress is not configured");
10892
+ }
10893
+ const now = Date.now();
10894
+ const token = createSandboxEgressRequesterToken({
10787
10895
  requesterId: credentialEgress.requesterId,
10788
- ttlMs: options?.timeoutMs
10896
+ egressId,
10897
+ ttlMs: sandboxEgressTokenTtlMs
10898
+ });
10899
+ sandboxEgressRequesterTokens.set(egressId, {
10900
+ expiresAtMs: now + sandboxEgressTokenTtlMs,
10901
+ token
10789
10902
  });
10790
- } : void 0;
10791
- const clearSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10792
- await clearSandboxEgressSession(egressId);
10793
- } : void 0;
10903
+ return token;
10904
+ };
10794
10905
  const sessionManager = createSandboxSessionManager({
10795
10906
  sandboxId: options?.sandboxId,
10796
10907
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
10797
10908
  timeoutMs: options?.timeoutMs,
10798
10909
  traceContext,
10799
10910
  commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
10800
- createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
10801
- beforeCommand: authorizeSandboxEgressForCommand,
10802
- afterCommand: clearSandboxEgressForCommand,
10911
+ createNetworkPolicy: credentialEgress ? (egressId) => buildSandboxEgressNetworkPolicy({
10912
+ requesterToken: sandboxEgressRequesterTokenFor(egressId)
10913
+ }) : void 0,
10803
10914
  onSandboxAcquired: async (sandbox) => {
10804
10915
  await options?.onSandboxAcquired?.(sandbox);
10805
10916
  }
@@ -11295,9 +11406,98 @@ async function unlinkProvider(userId, provider, userTokenStore) {
11295
11406
  ]);
11296
11407
  }
11297
11408
 
11409
+ // src/chat/state/turn-session-store.ts
11410
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
11411
+
11412
+ // src/chat/state/pi-session-message-store.ts
11413
+ import { isDeepStrictEqual } from "util";
11414
+ var PI_SESSION_MESSAGE_PREFIX = "junior:pi_session_message";
11415
+ function piSessionMessageKey(scope, index) {
11416
+ return `${PI_SESSION_MESSAGE_PREFIX}:${scope.conversationId}:${scope.sessionId}:${index}`;
11417
+ }
11418
+ function parsePiMessage(value) {
11419
+ return isRecord(value) ? value : void 0;
11420
+ }
11421
+ function normalizeMessageCount(value) {
11422
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
11423
+ }
11424
+ function countMatchingPrefix(left, right) {
11425
+ const limit = Math.min(left.length, right.length);
11426
+ for (let index = 0; index < limit; index += 1) {
11427
+ if (!isDeepStrictEqual(left[index], right[index])) {
11428
+ return index;
11429
+ }
11430
+ }
11431
+ return limit;
11432
+ }
11433
+ async function loadPiSessionMessages(args) {
11434
+ const stateAdapter = getStateAdapter();
11435
+ await stateAdapter.connect();
11436
+ const messageCount = normalizeMessageCount(args.messageCount);
11437
+ if (messageCount === 0) {
11438
+ return [];
11439
+ }
11440
+ const values = await Promise.all(
11441
+ Array.from(
11442
+ { length: messageCount },
11443
+ (_, index) => stateAdapter.get(piSessionMessageKey(args, index))
11444
+ )
11445
+ );
11446
+ const messages = [];
11447
+ for (const value of values) {
11448
+ const message = parsePiMessage(value);
11449
+ if (!message) {
11450
+ break;
11451
+ }
11452
+ messages.push(message);
11453
+ }
11454
+ return messages.length === messageCount ? messages : void 0;
11455
+ }
11456
+ async function loadExistingPiSessionMessages(scope, maxCount) {
11457
+ const count = normalizeMessageCount(maxCount);
11458
+ if (count === 0) {
11459
+ return [];
11460
+ }
11461
+ const stateAdapter = getStateAdapter();
11462
+ await stateAdapter.connect();
11463
+ const values = await Promise.all(
11464
+ Array.from(
11465
+ { length: count },
11466
+ (_, index) => stateAdapter.get(piSessionMessageKey(scope, index))
11467
+ )
11468
+ );
11469
+ const messages = [];
11470
+ for (const value of values) {
11471
+ const message = parsePiMessage(value);
11472
+ if (!message) {
11473
+ break;
11474
+ }
11475
+ messages.push(message);
11476
+ }
11477
+ return messages;
11478
+ }
11479
+ async function commitPiSessionMessages(args) {
11480
+ const stateAdapter = getStateAdapter();
11481
+ await stateAdapter.connect();
11482
+ const existingMessages = await loadExistingPiSessionMessages(
11483
+ { conversationId: args.conversationId, sessionId: args.sessionId },
11484
+ args.messages.length
11485
+ );
11486
+ const writeFromIndex = countMatchingPrefix(existingMessages, args.messages);
11487
+ await Promise.all(
11488
+ args.messages.slice(writeFromIndex).map(
11489
+ (message, offset) => stateAdapter.set(
11490
+ piSessionMessageKey(args, writeFromIndex + offset),
11491
+ message,
11492
+ args.ttlMs
11493
+ )
11494
+ )
11495
+ );
11496
+ }
11497
+
11298
11498
  // src/chat/state/turn-session-store.ts
11299
11499
  var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
11300
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
11500
+ var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS3;
11301
11501
  function agentTurnSessionKey(conversationId, sessionId) {
11302
11502
  return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
11303
11503
  }
@@ -11323,41 +11523,55 @@ function parseAgentTurnUsage(value) {
11323
11523
  }
11324
11524
  return Object.keys(usage).length > 0 ? usage : void 0;
11325
11525
  }
11326
- function parseAgentTurnSessionCheckpoint(value) {
11526
+ function parseStoredRecord(value) {
11527
+ if (isRecord(value)) {
11528
+ return value;
11529
+ }
11327
11530
  if (typeof value !== "string") {
11328
11531
  return void 0;
11329
11532
  }
11330
11533
  try {
11331
11534
  const parsed = JSON.parse(value);
11332
- if (!isRecord(parsed)) {
11333
- return void 0;
11334
- }
11335
- const status = parsed.state;
11336
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11337
- return void 0;
11338
- }
11339
- const conversationId = parsed.conversationId;
11340
- const sessionId = parsed.sessionId;
11341
- const sliceId = parsed.sliceId;
11342
- const checkpointVersion = parsed.checkpointVersion;
11343
- const updatedAtMs = parsed.updatedAtMs;
11344
- const cumulativeDurationMs = toFiniteNonNegativeNumber(
11345
- parsed.cumulativeDurationMs
11346
- );
11347
- const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
11348
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11349
- return void 0;
11350
- }
11351
- return {
11535
+ return isRecord(parsed) ? parsed : void 0;
11536
+ } catch {
11537
+ return void 0;
11538
+ }
11539
+ }
11540
+ function parseAgentTurnSessionRecord(value) {
11541
+ const parsed = parseStoredRecord(value);
11542
+ if (!parsed) {
11543
+ return void 0;
11544
+ }
11545
+ const status = parsed.state;
11546
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11547
+ return void 0;
11548
+ }
11549
+ const conversationId = parsed.conversationId;
11550
+ const sessionId = parsed.sessionId;
11551
+ const sliceId = parsed.sliceId;
11552
+ const checkpointVersion = parsed.checkpointVersion;
11553
+ const updatedAtMs = parsed.updatedAtMs;
11554
+ const cumulativeDurationMs = toFiniteNonNegativeNumber(
11555
+ parsed.cumulativeDurationMs
11556
+ );
11557
+ const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
11558
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11559
+ return void 0;
11560
+ }
11561
+ const legacyPiMessages = Array.isArray(parsed.piMessages) ? parsed.piMessages : [];
11562
+ const messageCount = toFiniteNonNegativeNumber(parsed.messageCount) ?? legacyPiMessages.length;
11563
+ return {
11564
+ legacyPiMessages,
11565
+ record: {
11352
11566
  checkpointVersion,
11353
11567
  conversationId,
11354
11568
  sessionId,
11355
11569
  sliceId,
11356
11570
  state: status,
11357
11571
  updatedAtMs,
11572
+ messageCount,
11358
11573
  ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
11359
11574
  ...cumulativeUsage ? { cumulativeUsage } : {},
11360
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
11361
11575
  ...Array.isArray(parsed.loadedSkillNames) ? {
11362
11576
  loadedSkillNames: parsed.loadedSkillNames.filter(
11363
11577
  (value2) => typeof value2 === "string"
@@ -11366,10 +11580,20 @@ function parseAgentTurnSessionCheckpoint(value) {
11366
11580
  ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
11367
11581
  ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
11368
11582
  ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
11369
- };
11370
- } catch {
11371
- return void 0;
11583
+ }
11584
+ };
11585
+ }
11586
+ function materializePiMessages(legacyPiMessages, messageCount, sessionMessages) {
11587
+ if (messageCount === 0) {
11588
+ return [];
11372
11589
  }
11590
+ if (sessionMessages) {
11591
+ return sessionMessages;
11592
+ }
11593
+ if (legacyPiMessages.length >= messageCount) {
11594
+ return legacyPiMessages.slice(0, messageCount);
11595
+ }
11596
+ return void 0;
11373
11597
  }
11374
11598
  async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11375
11599
  const stateAdapter = getStateAdapter();
@@ -11377,23 +11601,51 @@ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11377
11601
  const value = await stateAdapter.get(
11378
11602
  agentTurnSessionKey(conversationId, sessionId)
11379
11603
  );
11380
- return parseAgentTurnSessionCheckpoint(value);
11604
+ const parsed = parseAgentTurnSessionRecord(value);
11605
+ if (!parsed) {
11606
+ return void 0;
11607
+ }
11608
+ const sessionMessages = await loadPiSessionMessages({
11609
+ conversationId,
11610
+ sessionId,
11611
+ messageCount: parsed.record.messageCount
11612
+ });
11613
+ const piMessages = materializePiMessages(
11614
+ parsed.legacyPiMessages,
11615
+ parsed.record.messageCount,
11616
+ sessionMessages
11617
+ );
11618
+ if (!piMessages) {
11619
+ return void 0;
11620
+ }
11621
+ return {
11622
+ ...parsed.record,
11623
+ piMessages
11624
+ };
11381
11625
  }
11382
11626
  async function upsertAgentTurnSessionCheckpoint(args) {
11383
11627
  const stateAdapter = getStateAdapter();
11384
11628
  await stateAdapter.connect();
11385
- const existing = await getAgentTurnSessionCheckpoint(
11386
- args.conversationId,
11387
- args.sessionId
11629
+ const existingValue = await stateAdapter.get(
11630
+ agentTurnSessionKey(args.conversationId, args.sessionId)
11388
11631
  );
11632
+ const existingRecord = parseAgentTurnSessionRecord(existingValue);
11633
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11634
+ await commitPiSessionMessages({
11635
+ conversationId: args.conversationId,
11636
+ sessionId: args.sessionId,
11637
+ messages: args.piMessages,
11638
+ ttlMs
11639
+ });
11640
+ const storedMessageCount = args.piMessages.length;
11389
11641
  const checkpoint = {
11390
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
11642
+ checkpointVersion: (existingRecord?.record.checkpointVersion ?? 0) + 1,
11391
11643
  conversationId: args.conversationId,
11392
11644
  sessionId: args.sessionId,
11393
11645
  sliceId: args.sliceId,
11394
11646
  state: args.state,
11395
11647
  updatedAtMs: Date.now(),
11396
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
11648
+ messageCount: storedMessageCount,
11397
11649
  ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
11398
11650
  cumulativeDurationMs: Math.max(
11399
11651
  0,
@@ -11410,13 +11662,15 @@ async function upsertAgentTurnSessionCheckpoint(args) {
11410
11662
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
11411
11663
  ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
11412
11664
  };
11413
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11414
11665
  await stateAdapter.set(
11415
11666
  agentTurnSessionKey(args.conversationId, args.sessionId),
11416
- JSON.stringify(checkpoint),
11667
+ checkpoint,
11417
11668
  ttlMs
11418
11669
  );
11419
- return checkpoint;
11670
+ return {
11671
+ ...checkpoint,
11672
+ piMessages: [...args.piMessages]
11673
+ };
11420
11674
  }
11421
11675
  async function supersedeAgentTurnSessionCheckpoint(args) {
11422
11676
  const existing = await getAgentTurnSessionCheckpoint(
@@ -11969,7 +12223,6 @@ function buildBriefPostCanvasReply(artifactStatePatch) {
11969
12223
  function buildTurnResult(input) {
11970
12224
  const {
11971
12225
  newMessages,
11972
- piMessages,
11973
12226
  userInput,
11974
12227
  replyFiles,
11975
12228
  artifactStatePatch,
@@ -12073,7 +12326,6 @@ function buildTurnResult(input) {
12073
12326
  text: resolvedText,
12074
12327
  files: replyFiles.length > 0 ? replyFiles : void 0,
12075
12328
  artifactStatePatch: Object.keys(artifactStatePatch).length > 0 ? artifactStatePatch : void 0,
12076
- piMessages,
12077
12329
  deliveryPlan,
12078
12330
  deliveryMode,
12079
12331
  sandboxId,
@@ -12756,69 +13008,6 @@ function buildUserTurnInput(args) {
12756
13008
  }
12757
13009
  return { routerBlocks, userContentParts };
12758
13010
  }
12759
- function refreshCheckpointTurnContext(messages, turnContextPrompt) {
12760
- const marker = getTurnContextMarker(turnContextPrompt);
12761
- for (let index = 0; index < messages.length; index += 1) {
12762
- const content = getUserMessageContent(messages[index]);
12763
- if (!content) {
12764
- continue;
12765
- }
12766
- const contextIndex = content.findIndex(
12767
- (part) => isTurnContextPart(part, marker)
12768
- );
12769
- if (contextIndex < 0) {
12770
- continue;
12771
- }
12772
- const updatedMessages = [...messages];
12773
- const updatedContent = [...content];
12774
- updatedContent[contextIndex] = {
12775
- ...updatedContent[contextIndex],
12776
- text: turnContextPrompt
12777
- };
12778
- updatedMessages[index] = {
12779
- ...messages[index],
12780
- content: updatedContent
12781
- };
12782
- return updatedMessages;
12783
- }
12784
- return [
12785
- ...messages,
12786
- {
12787
- role: "user",
12788
- content: [{ type: "text", text: turnContextPrompt }],
12789
- timestamp: Date.now()
12790
- }
12791
- ];
12792
- }
12793
- function stripTurnContextFromMessages(messages, turnContextPrompt) {
12794
- const marker = getTurnContextMarker(turnContextPrompt);
12795
- return messages.flatMap((message) => {
12796
- const content = getUserMessageContent(message);
12797
- if (!content) {
12798
- return [message];
12799
- }
12800
- const strippedContent = content.filter(
12801
- (part) => !isTurnContextPart(part, marker)
12802
- );
12803
- if (strippedContent.length === content.length) {
12804
- return [message];
12805
- }
12806
- if (strippedContent.length === 0) {
12807
- return [];
12808
- }
12809
- return [{ ...message, content: strippedContent }];
12810
- });
12811
- }
12812
- function getTurnContextMarker(turnContextPrompt) {
12813
- return turnContextPrompt.split("\n", 1)[0];
12814
- }
12815
- function getUserMessageContent(message) {
12816
- const record = message;
12817
- return record.role === "user" && Array.isArray(record.content) ? record.content : void 0;
12818
- }
12819
- function isTurnContextPart(part, marker) {
12820
- return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.startsWith(marker);
12821
- }
12822
13011
  async function generateAssistantReply(messageText, context = {}) {
12823
13012
  const replyStartedAtMs = Date.now();
12824
13013
  let timeoutResumeConversationId;
@@ -13331,7 +13520,7 @@ async function generateAssistantReply(messageText, context = {}) {
13331
13520
  beforeMessageCount = agent.state.messages.length;
13332
13521
  try {
13333
13522
  if (resumedFromCheckpoint) {
13334
- agent.state.messages = refreshCheckpointTurnContext(
13523
+ agent.state.messages = refreshRuntimeTurnContext(
13335
13524
  existingCheckpoint.piMessages,
13336
13525
  turnContextPrompt
13337
13526
  );
@@ -13444,10 +13633,6 @@ async function generateAssistantReply(messageText, context = {}) {
13444
13633
  }
13445
13634
  return buildTurnResult({
13446
13635
  newMessages,
13447
- piMessages: stripTurnContextFromMessages(
13448
- agent.state.messages,
13449
- turnContextPrompt
13450
- ),
13451
13636
  userInput,
13452
13637
  replyFiles,
13453
13638
  artifactStatePatch,
@@ -14909,7 +15094,7 @@ function completeAuthPauseTurn(args) {
14909
15094
  skippedReason: void 0
14910
15095
  }
14911
15096
  );
14912
- markTurnCompleted({
15097
+ markTurnClosed({
14913
15098
  conversation: args.conversation,
14914
15099
  nowMs: Date.now(),
14915
15100
  sessionId: args.sessionId,
@@ -14927,7 +15112,7 @@ async function persistAuthPauseTurnState(args) {
14927
15112
  }
14928
15113
 
14929
15114
  // src/chat/services/timeout-resume.ts
14930
- import { createHmac, timingSafeEqual } from "crypto";
15115
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
14931
15116
  var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
14932
15117
  var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
14933
15118
  var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
@@ -14962,16 +15147,16 @@ function buildSignedPayload(timestamp, body) {
14962
15147
  return `${timestamp}:${body}`;
14963
15148
  }
14964
15149
  function signTurnTimeoutResumeBody(secret, timestamp, body) {
14965
- const digest = createHmac("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
15150
+ const digest = createHmac2("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
14966
15151
  return `${TURN_TIMEOUT_RESUME_SIGNATURE_VERSION}=${digest}`;
14967
15152
  }
14968
- function timingSafeMatch(expected, actual) {
15153
+ function timingSafeMatch2(expected, actual) {
14969
15154
  const expectedBuffer = Buffer.from(expected);
14970
15155
  const actualBuffer = Buffer.from(actual);
14971
15156
  if (expectedBuffer.length !== actualBuffer.length) {
14972
15157
  return false;
14973
15158
  }
14974
- return timingSafeEqual(expectedBuffer, actualBuffer);
15159
+ return timingSafeEqual2(expectedBuffer, actualBuffer);
14975
15160
  }
14976
15161
  function parseTurnTimeoutResumeRequest(value) {
14977
15162
  if (!value || typeof value !== "object") {
@@ -15034,7 +15219,7 @@ async function verifyTurnTimeoutResumeRequest(request) {
15034
15219
  }
15035
15220
  const body = await request.text();
15036
15221
  const expectedSignature = signTurnTimeoutResumeBody(secret, timestamp, body);
15037
- if (!timingSafeMatch(expectedSignature, signature)) {
15222
+ if (!timingSafeMatch2(expectedSignature, signature)) {
15038
15223
  return void 0;
15039
15224
  }
15040
15225
  try {
@@ -15111,9 +15296,9 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
15111
15296
  const conversation = coerceThreadConversationState(currentState);
15112
15297
  const artifacts = coerceThreadArtifactsState(currentState);
15113
15298
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
15114
- const userMessageId = getTurnUserMessageId(conversation, sessionId);
15299
+ const userMessage = getTurnUserMessage(conversation, sessionId);
15115
15300
  clearPendingAuth(conversation, sessionId);
15116
- markConversationMessage(conversation, userMessageId, {
15301
+ markConversationMessage(conversation, userMessage?.id, {
15117
15302
  replied: true,
15118
15303
  skippedReason: void 0
15119
15304
  });
@@ -15130,9 +15315,6 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
15130
15315
  replied: true
15131
15316
  }
15132
15317
  });
15133
- if (reply.piMessages) {
15134
- conversation.piMessages = reply.piMessages;
15135
- }
15136
15318
  markTurnCompleted({
15137
15319
  conversation,
15138
15320
  nowMs: Date.now(),
@@ -15554,9 +15736,6 @@ async function persistCompletedOAuthReplyState(args) {
15554
15736
  replied: true
15555
15737
  }
15556
15738
  });
15557
- if (args.reply.piMessages) {
15558
- conversation.piMessages = args.reply.piMessages;
15559
- }
15560
15739
  markTurnCompleted({
15561
15740
  conversation,
15562
15741
  nowMs: Date.now(),
@@ -16069,10 +16248,32 @@ function egressAttributes(input) {
16069
16248
  ...input.status ? { "http.response.status_code": input.status } : {}
16070
16249
  };
16071
16250
  }
16251
+ function requesterTokenFromRequest(request) {
16252
+ const pathname = new URL(request.url).pathname;
16253
+ const prefix = `${SANDBOX_EGRESS_PROXY_PATH}/`;
16254
+ if (!pathname.startsWith(prefix)) {
16255
+ return void 0;
16256
+ }
16257
+ const token = pathname.slice(prefix.length).split("/")[0];
16258
+ if (!token) {
16259
+ return void 0;
16260
+ }
16261
+ try {
16262
+ return decodeURIComponent(token);
16263
+ } catch {
16264
+ return void 0;
16265
+ }
16266
+ }
16267
+ function redactedProxyPath(pathname) {
16268
+ if (pathname.startsWith(`${SANDBOX_EGRESS_PROXY_PATH}/`)) {
16269
+ return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
16270
+ }
16271
+ return pathname;
16272
+ }
16072
16273
  function routingAttributes(request, upstreamUrl) {
16073
16274
  const proxyUrl = new URL(request.url);
16074
16275
  const attributes = {
16075
- "app.sandbox.egress.proxy_path": proxyUrl.pathname
16276
+ "app.sandbox.egress.proxy_path": redactedProxyPath(proxyUrl.pathname)
16076
16277
  };
16077
16278
  if (upstreamUrl) {
16078
16279
  attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
@@ -16125,22 +16326,23 @@ function normalizePort(value) {
16125
16326
  function sandboxIdFromPayload(payload) {
16126
16327
  return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
16127
16328
  }
16329
+ function normalizedForwardedPath(path11) {
16330
+ if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
16331
+ return { ok: false, error: "Invalid forwarded path" };
16332
+ }
16333
+ try {
16334
+ const url = new URL(path11, "https://sandbox-forwarded.local");
16335
+ return { ok: true, path: `${url.pathname}${url.search}` };
16336
+ } catch {
16337
+ return { ok: false, error: "Invalid forwarded path" };
16338
+ }
16339
+ }
16128
16340
  function upstreamPath(request) {
16129
16341
  const forwardedPath = request.headers.get(FORWARDED_PATH_HEADER);
16130
- if (forwardedPath?.trim()) {
16131
- const path11 = forwardedPath.trim();
16132
- if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
16133
- return { ok: false, error: "Invalid forwarded path" };
16134
- }
16135
- try {
16136
- const url2 = new URL(path11, "https://sandbox-forwarded.local");
16137
- return { ok: true, path: `${url2.pathname}${url2.search}` };
16138
- } catch {
16139
- return { ok: false, error: "Invalid forwarded path" };
16140
- }
16342
+ if (!forwardedPath?.trim()) {
16343
+ return { ok: false, error: "Missing forwarded path" };
16141
16344
  }
16142
- const url = new URL(request.url);
16143
- return { ok: true, path: `${url.pathname}${url.search}` };
16345
+ return normalizedForwardedPath(forwardedPath.trim());
16144
16346
  }
16145
16347
  function buildUpstreamUrl(request) {
16146
16348
  const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
@@ -16212,18 +16414,14 @@ function responseHeaders(upstream) {
16212
16414
  });
16213
16415
  return headers;
16214
16416
  }
16215
- async function credentialLease(egressId, provider, session) {
16216
- const cached = await getSandboxEgressCredentialLease(
16217
- egressId,
16218
- provider,
16219
- session
16220
- );
16417
+ async function credentialLease(provider, context) {
16418
+ const cached = await getSandboxEgressCredentialLease(provider, context);
16221
16419
  if (cached) {
16222
16420
  return cached;
16223
16421
  }
16224
16422
  const lease = await issueProviderCredentialLease({
16225
16423
  provider,
16226
- requesterId: session.requesterId,
16424
+ requesterId: context.requesterId,
16227
16425
  reason: `sandbox-egress:${provider}`
16228
16426
  });
16229
16427
  const headerTransforms = lease.headerTransforms ?? [];
@@ -16237,7 +16435,7 @@ async function credentialLease(egressId, provider, session) {
16237
16435
  expiresAt: lease.expiresAt,
16238
16436
  headerTransforms
16239
16437
  };
16240
- await setSandboxEgressCredentialLease(egressId, session, cachedLease);
16438
+ await setSandboxEgressCredentialLease(context, cachedLease);
16241
16439
  return cachedLease;
16242
16440
  }
16243
16441
  function hasTransformForHost(lease, host) {
@@ -16278,7 +16476,7 @@ async function proxySandboxEgressRequest(request, deps = {}) {
16278
16476
  {},
16279
16477
  {
16280
16478
  "http.request.method": request.method,
16281
- "url.path": new URL(request.url).pathname
16479
+ "url.path": redactedProxyPath(new URL(request.url).pathname)
16282
16480
  },
16283
16481
  "Sandbox egress OIDC payload did not include a VM session id"
16284
16482
  );
@@ -16296,7 +16494,7 @@ async function proxySandboxEgressRequest(request, deps = {}) {
16296
16494
  ...egressAttributes({
16297
16495
  egressId: activeEgressId,
16298
16496
  method: request.method,
16299
- path: new URL(request.url).pathname,
16497
+ path: redactedProxyPath(new URL(request.url).pathname),
16300
16498
  status: 400
16301
16499
  }),
16302
16500
  ...routingAttributes(request)
@@ -16325,10 +16523,12 @@ async function proxySandboxEgressRequest(request, deps = {}) {
16325
16523
  );
16326
16524
  return jsonError("No provider owns forwarded host", 403);
16327
16525
  }
16328
- const session = await getSandboxEgressSession(activeEgressId);
16329
- if (!session) {
16526
+ const requesterContext = parseSandboxEgressRequesterToken(
16527
+ requesterTokenFromRequest(request)
16528
+ );
16529
+ if (!requesterContext || requesterContext.egressId !== activeEgressId) {
16330
16530
  logWarn(
16331
- "sandbox_egress_session_unauthorized",
16531
+ "sandbox_egress_requester_context_unauthorized",
16332
16532
  {},
16333
16533
  {
16334
16534
  ...egressAttributes({
@@ -16341,13 +16541,13 @@ async function proxySandboxEgressRequest(request, deps = {}) {
16341
16541
  }),
16342
16542
  ...routingAttributes(request, upstreamUrl)
16343
16543
  },
16344
- "Sandbox egress VM session is not authorized for requester credentials"
16544
+ "Sandbox egress request did not include a valid requester context for the VM session"
16345
16545
  );
16346
- return jsonError("Sandbox egress session is not authorized", 403);
16546
+ return jsonError("Sandbox egress requester context is not authorized", 403);
16347
16547
  }
16348
16548
  let lease;
16349
16549
  try {
16350
- lease = await credentialLease(activeEgressId, provider, session);
16550
+ lease = await credentialLease(provider, requesterContext);
16351
16551
  } catch (error) {
16352
16552
  if (error instanceof CredentialUnavailableError) {
16353
16553
  logWarn(
@@ -16451,7 +16651,7 @@ ${error.message}`,
16451
16651
  },
16452
16652
  "Sandbox egress upstream auth rejected"
16453
16653
  );
16454
- await clearSandboxEgressCredentialLease(activeEgressId, provider, session);
16654
+ await clearSandboxEgressCredentialLease(provider, requesterContext);
16455
16655
  }
16456
16656
  return new Response(upstream.body, {
16457
16657
  status: upstream.status,
@@ -16502,9 +16702,6 @@ async function persistCompletedReplyState2(args) {
16502
16702
  replied: true
16503
16703
  }
16504
16704
  });
16505
- if (args.reply.piMessages) {
16506
- conversation.piMessages = args.reply.piMessages;
16507
- }
16508
16705
  markTurnCompleted({
16509
16706
  conversation,
16510
16707
  nowMs: Date.now(),
@@ -18208,6 +18405,31 @@ function getCurrentTurnCanvasUrl(args) {
18208
18405
  function buildCanvasRecoveryReply(canvasUrl) {
18209
18406
  return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
18210
18407
  }
18408
+ async function loadPiMessagesForTurn(args) {
18409
+ const fallback = args.fallback.length > 0 ? [...args.fallback] : void 0;
18410
+ if (!args.conversationId) {
18411
+ return fallback;
18412
+ }
18413
+ if (args.activeTurnId) {
18414
+ const checkpoint2 = await getAgentTurnSessionCheckpoint(
18415
+ args.conversationId,
18416
+ args.activeTurnId
18417
+ );
18418
+ if (checkpoint2?.piMessages.length) {
18419
+ return stripRuntimeTurnContext(
18420
+ trimTrailingAssistantMessages(checkpoint2.piMessages)
18421
+ );
18422
+ }
18423
+ }
18424
+ if (!args.lastSessionId) {
18425
+ return fallback;
18426
+ }
18427
+ const checkpoint = await getAgentTurnSessionCheckpoint(
18428
+ args.conversationId,
18429
+ args.lastSessionId
18430
+ );
18431
+ return checkpoint?.state === "completed" && checkpoint.piMessages.length > 0 ? stripRuntimeTurnContext(checkpoint.piMessages) : fallback;
18432
+ }
18211
18433
  function createReplyToThread(deps) {
18212
18434
  return async function replyToThread(thread, message, options = {}) {
18213
18435
  if (message.author.isMe) {
@@ -18360,6 +18582,7 @@ function createReplyToThread(deps) {
18360
18582
  return;
18361
18583
  }
18362
18584
  }
18585
+ const lastSessionIdForHistory = preparedState.conversation.processing.lastSessionId;
18363
18586
  const configReply = await maybeApplyProviderDefaultConfigRequest({
18364
18587
  channelConfiguration: preparedState.channelConfiguration,
18365
18588
  requesterId: message.author.userId,
@@ -18435,6 +18658,12 @@ function createReplyToThread(deps) {
18435
18658
  }
18436
18659
  );
18437
18660
  const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
18661
+ const piMessages = await loadPiMessagesForTurn({
18662
+ conversationId,
18663
+ activeTurnId,
18664
+ lastSessionId: lastSessionIdForHistory,
18665
+ fallback: preparedState.conversation.piMessages
18666
+ });
18438
18667
  const status = createSlackAdapterAssistantStatusSession({
18439
18668
  channelId: assistantThreadContext?.channelId,
18440
18669
  threadTs: assistantThreadContext?.threadTs,
@@ -18486,7 +18715,7 @@ function createReplyToThread(deps) {
18486
18715
  },
18487
18716
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
18488
18717
  artifactState: preparedState.artifacts,
18489
- piMessages: preparedState.conversation.piMessages,
18718
+ piMessages,
18490
18719
  pendingAuth: preparedState.conversation.processing.pendingAuth,
18491
18720
  configuration: preparedState.configuration,
18492
18721
  channelConfiguration: preparedState.channelConfiguration,
@@ -18569,9 +18798,6 @@ function createReplyToThread(deps) {
18569
18798
  replied: true
18570
18799
  }
18571
18800
  });
18572
- if (reply.piMessages) {
18573
- preparedState.conversation.piMessages = reply.piMessages;
18574
- }
18575
18801
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
18576
18802
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
18577
18803
  "slackMessageAddReaction"
@@ -18649,6 +18875,7 @@ function createReplyToThread(deps) {
18649
18875
  markTurnCompleted({
18650
18876
  conversation: preparedState.conversation,
18651
18877
  nowMs: Date.now(),
18878
+ sessionId: turnId,
18652
18879
  updateConversationStats
18653
18880
  });
18654
18881
  await persistThreadState(thread, {
@@ -18774,9 +19001,10 @@ function createReplyToThread(deps) {
18774
19001
  replied: true
18775
19002
  }
18776
19003
  });
18777
- markTurnCompleted({
19004
+ markTurnClosed({
18778
19005
  conversation: preparedState.conversation,
18779
19006
  nowMs: Date.now(),
19007
+ sessionId: turnId,
18780
19008
  updateConversationStats
18781
19009
  });
18782
19010
  await persistThreadState(thread, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"