@sentry/junior 0.21.0 → 0.22.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
@@ -6,7 +6,7 @@ import {
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
8
  parseSkillInvocation
9
- } from "./chunk-NRSP2MLC.js";
9
+ } from "./chunk-JWBWBJYJ.js";
10
10
  import {
11
11
  SANDBOX_DATA_ROOT,
12
12
  SANDBOX_SKILLS_ROOT,
@@ -27,10 +27,11 @@ import {
27
27
  sandboxSkillDir,
28
28
  sandboxSkillFile,
29
29
  toOptionalTrimmed
30
- } from "./chunk-Z43DS7XN.js";
30
+ } from "./chunk-THPM7NSG.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  buildOAuthTokenRequest,
34
+ createChatSdkLogger,
34
35
  createPluginBroker,
35
36
  createRequestContext,
36
37
  extractGenAiUsageAttributes,
@@ -57,7 +58,7 @@ import {
57
58
  toOptionalString,
58
59
  withContext,
59
60
  withSpan
60
- } from "./chunk-N4ICA2BC.js";
61
+ } from "./chunk-MCJJKEB3.js";
61
62
  import "./chunk-Z3YD6NHK.js";
62
63
  import {
63
64
  discoverInstalledPluginPackageContent,
@@ -1786,7 +1787,7 @@ function validateConfigKey(key) {
1786
1787
  return "Configuration key must not be empty";
1787
1788
  }
1788
1789
  if (!CONFIG_KEY_RE.test(trimmed)) {
1789
- return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "github.repo")`;
1790
+ return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "provider.repo")`;
1790
1791
  }
1791
1792
  if (SECRET_KEY_RE.test(trimmed)) {
1792
1793
  return `Configuration key "${key}" appears to be secret-related and is not allowed`;
@@ -1808,7 +1809,9 @@ function collectStringValues(value, output, depth = 0) {
1808
1809
  return;
1809
1810
  }
1810
1811
  if (value && typeof value === "object") {
1811
- for (const [key, nested] of Object.entries(value)) {
1812
+ for (const [key, nested] of Object.entries(
1813
+ value
1814
+ )) {
1812
1815
  output.push(key);
1813
1816
  collectStringValues(nested, output, depth + 1);
1814
1817
  }
@@ -2016,6 +2019,9 @@ function buildArtifactStatePatch(patch) {
2016
2019
  function threadStateKey(threadId) {
2017
2020
  return `thread-state:${threadId}`;
2018
2021
  }
2022
+ function channelStateKey(channelId) {
2023
+ return `channel-state:${channelId}`;
2024
+ }
2019
2025
  function buildThreadStatePayload(patch) {
2020
2026
  const payload = {};
2021
2027
  if (patch.artifacts) {
@@ -2045,6 +2051,14 @@ function mergeArtifactsState(artifacts, patch) {
2045
2051
  }
2046
2052
  };
2047
2053
  }
2054
+ function getPersistedSandboxState(state) {
2055
+ return {
2056
+ sandboxId: toOptionalString(state.app_sandbox_id),
2057
+ sandboxDependencyProfileHash: toOptionalString(
2058
+ state.app_sandbox_dependency_profile_hash
2059
+ )
2060
+ };
2061
+ }
2048
2062
  async function persistThreadState(thread, patch) {
2049
2063
  const payload = buildThreadStatePayload(patch);
2050
2064
  if (Object.keys(payload).length === 0) {
@@ -2059,6 +2073,13 @@ async function getPersistedThreadState(threadId) {
2059
2073
  threadStateKey(threadId)
2060
2074
  ) ?? {};
2061
2075
  }
2076
+ async function getPersistedChannelState(channelId) {
2077
+ const stateAdapter = getStateAdapter();
2078
+ await stateAdapter.connect();
2079
+ return await stateAdapter.get(
2080
+ channelStateKey(channelId)
2081
+ ) ?? {};
2082
+ }
2062
2083
  async function persistThreadStateById(threadId, patch) {
2063
2084
  const payload = buildThreadStatePayload(patch);
2064
2085
  if (Object.keys(payload).length === 0) {
@@ -2081,6 +2102,115 @@ function getChannelConfigurationService(thread) {
2081
2102
  }
2082
2103
  });
2083
2104
  }
2105
+ function getChannelConfigurationServiceById(channelId) {
2106
+ return createChannelConfigurationService({
2107
+ load: async () => await getPersistedChannelState(channelId),
2108
+ save: async (state) => {
2109
+ const stateAdapter = getStateAdapter();
2110
+ await stateAdapter.connect();
2111
+ const key = channelStateKey(channelId);
2112
+ const existing = await stateAdapter.get(key) ?? {};
2113
+ await stateAdapter.set(
2114
+ key,
2115
+ { ...existing, configuration: state },
2116
+ THREAD_STATE_TTL_MS
2117
+ );
2118
+ }
2119
+ });
2120
+ }
2121
+
2122
+ // src/chat/runtime/thread-participants.ts
2123
+ function buildThreadParticipants(messages) {
2124
+ const seen = /* @__PURE__ */ new Set();
2125
+ const participants = [];
2126
+ for (const message of messages) {
2127
+ const { userId, userName, fullName } = message.author ?? {};
2128
+ if (!userId || message.author?.isBot) continue;
2129
+ if (seen.has(userId)) continue;
2130
+ seen.add(userId);
2131
+ participants.push({ userId, userName, fullName });
2132
+ }
2133
+ return participants;
2134
+ }
2135
+
2136
+ // src/chat/runtime/turn.ts
2137
+ function buildDeterministicTurnId(messageId) {
2138
+ const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
2139
+ return `turn_${sanitized}`;
2140
+ }
2141
+ var RetryableTurnError = class extends Error {
2142
+ code = "retryable_turn";
2143
+ metadata;
2144
+ reason;
2145
+ constructor(reason, message, metadata) {
2146
+ super(message);
2147
+ this.name = "RetryableTurnError";
2148
+ this.reason = reason;
2149
+ this.metadata = metadata;
2150
+ }
2151
+ };
2152
+ function isRetryableTurnError(error, reason) {
2153
+ if (!(error instanceof RetryableTurnError)) {
2154
+ return false;
2155
+ }
2156
+ if (!reason) {
2157
+ return true;
2158
+ }
2159
+ return error.reason === reason;
2160
+ }
2161
+ function startActiveTurn(args) {
2162
+ args.conversation.processing.activeTurnId = args.nextTurnId;
2163
+ args.updateConversationStats(args.conversation);
2164
+ }
2165
+ function markTurnCompleted(args) {
2166
+ args.conversation.processing.activeTurnId = void 0;
2167
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2168
+ args.updateConversationStats(args.conversation);
2169
+ }
2170
+ function markTurnFailed(args) {
2171
+ args.conversation.processing.activeTurnId = void 0;
2172
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2173
+ args.markConversationMessage(args.conversation, args.userMessageId, {
2174
+ replied: false,
2175
+ skippedReason: "reply failed"
2176
+ });
2177
+ args.updateConversationStats(args.conversation);
2178
+ }
2179
+ function resolveReplyDelivery(args) {
2180
+ const replyHasFiles = Boolean(
2181
+ args.reply.files && args.reply.files.length > 0
2182
+ );
2183
+ const deliveryPlan = args.reply.deliveryPlan ?? {
2184
+ mode: args.reply.deliveryMode ?? "thread",
2185
+ postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
2186
+ attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
2187
+ };
2188
+ let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
2189
+ if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
2190
+ attachFiles = "inline";
2191
+ }
2192
+ return {
2193
+ shouldPostThreadReply: deliveryPlan.postThreadText,
2194
+ attachFiles
2195
+ };
2196
+ }
2197
+
2198
+ // src/chat/runtime/turn-user-message.ts
2199
+ function getTurnUserMessage(conversation, sessionId) {
2200
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2201
+ const message = conversation.messages[index];
2202
+ if (message?.role !== "user") {
2203
+ continue;
2204
+ }
2205
+ if (buildDeterministicTurnId(message.id) === sessionId) {
2206
+ return message;
2207
+ }
2208
+ }
2209
+ return void 0;
2210
+ }
2211
+ function getTurnUserMessageId(conversation, sessionId) {
2212
+ return getTurnUserMessage(conversation, sessionId)?.id;
2213
+ }
2084
2214
 
2085
2215
  // src/chat/pi/client.ts
2086
2216
  import {
@@ -3055,14 +3185,14 @@ function buildSystemPrompt(params) {
3055
3185
  "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
3056
3186
  "- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `eyes`, not unicode emoji), and avoid redundant acknowledgment text.",
3057
3187
  "- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
3058
- "- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before authenticated bash/API work for that skill.",
3188
+ "- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--target <value>]` as a bash command before authenticated bash/API work for that skill.",
3059
3189
  "- Use the minimum declared capability needed for the current operation.",
3060
3190
  "- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
3061
3191
  "- For disconnect + reconnect requests, run `jr-rpc delete-token <provider>` first, then `jr-rpc issue-credential` \u2014 the system handles the reconnect without auto-resuming the reconnect message.",
3062
3192
  "- Use `jr-rpc oauth-start <provider>` only when the user explicitly asks to connect a provider and there is no task to resume after authorization.",
3063
- "- GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
3064
- "- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
3065
- "- Capabilities are provider-qualified (for example `github.issues.write`).",
3193
+ "- Provider-targeted capabilities may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3194
+ "- To persist or read conversation defaults, choose a config key from the provider catalog or active skill metadata and run `jr-rpc config get|set|unset|list ...` as a bash command.",
3195
+ "- Capabilities must match the exact provider-qualified tokens declared by the loaded skill or provider catalog.",
3066
3196
  "- When your work is complete, provide the exact user-facing markdown response.",
3067
3197
  "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3068
3198
  "- Prefer `webSearch` before `webFetch` when the user gave no URL.",
@@ -3134,40 +3264,49 @@ var ProviderCredentialRouter = class {
3134
3264
  };
3135
3265
 
3136
3266
  // src/chat/capabilities/target.ts
3137
- var REPO_FLAG_RE = /(?:^|\s)--repo(?:\s+|=)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#[0-9]+)?)/;
3138
- function parseRepoTarget(value) {
3139
- const trimmed = value.trim();
3140
- if (!trimmed) {
3267
+ function escapeRegExp(value) {
3268
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3269
+ }
3270
+ function normalizeTargetValue(value) {
3271
+ let normalized = value.trim();
3272
+ if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
3273
+ normalized = normalized.slice(1, -1).trim();
3274
+ }
3275
+ return normalized || void 0;
3276
+ }
3277
+ function extractFlagValue(text, flags) {
3278
+ if (flags.length === 0) {
3141
3279
  return void 0;
3142
3280
  }
3143
- const [repoRef] = trimmed.split("#");
3144
- const [owner, repo] = repoRef.split("/");
3145
- if (!owner || !repo) {
3281
+ const pattern = flags.map(escapeRegExp).join("|");
3282
+ const match = new RegExp(
3283
+ String.raw`(?:^|\s)(?:${pattern})(?:\s+|=)([^\s]+)`
3284
+ ).exec(text);
3285
+ return match ? normalizeTargetValue(match[1] ?? "") : void 0;
3286
+ }
3287
+ function createCapabilityTarget(type, value) {
3288
+ const normalizedType = type.trim();
3289
+ const normalizedValue = normalizeTargetValue(value);
3290
+ if (!normalizedType || !normalizedValue) {
3146
3291
  return void 0;
3147
3292
  }
3148
3293
  return {
3149
- owner: owner.toLowerCase(),
3150
- repo: repo.toLowerCase()
3294
+ type: normalizedType,
3295
+ value: normalizedValue
3151
3296
  };
3152
3297
  }
3153
- function extractRepoRef(text) {
3154
- const byFlag = REPO_FLAG_RE.exec(text);
3155
- if (byFlag) {
3156
- return parseRepoTarget(byFlag[1]);
3157
- }
3158
- return void 0;
3159
- }
3160
3298
  function extractCapabilityTarget(params) {
3299
+ const flags = params.target.commandFlags ?? [];
3161
3300
  if (params.commandText) {
3162
- const commandRepo = extractRepoRef(params.commandText);
3163
- if (commandRepo) {
3164
- return commandRepo;
3301
+ const value = extractFlagValue(params.commandText, flags);
3302
+ if (value) {
3303
+ return createCapabilityTarget(params.target.type, value);
3165
3304
  }
3166
3305
  }
3167
3306
  if (params.invocationArgs) {
3168
- const invocationRepo = extractRepoRef(params.invocationArgs);
3169
- if (invocationRepo) {
3170
- return invocationRepo;
3307
+ const value = extractFlagValue(params.invocationArgs, flags);
3308
+ if (value) {
3309
+ return createCapabilityTarget(params.target.type, value);
3171
3310
  }
3172
3311
  }
3173
3312
  return void 0;
@@ -3198,44 +3337,50 @@ var SkillCapabilityRuntime = class {
3198
3337
  }
3199
3338
  async resolveCapabilityTarget(input) {
3200
3339
  const activeSkill = input.activeSkill;
3201
- const explicitTarget = input.repoRef ? parseRepoTarget(input.repoRef) : void 0;
3340
+ const explicitTarget = input.targetRef ? createCapabilityTarget(input.target.type, input.targetRef) : void 0;
3202
3341
  if (explicitTarget) {
3203
3342
  return explicitTarget;
3204
3343
  }
3205
3344
  const inferredTarget = extractCapabilityTarget({
3206
- invocationArgs: this.invocationArgs
3345
+ invocationArgs: this.invocationArgs,
3346
+ target: input.target
3207
3347
  });
3208
3348
  if (inferredTarget) {
3209
3349
  return inferredTarget;
3210
3350
  }
3211
- if (!input.configKey || !this.resolveConfiguration) {
3351
+ if (!this.resolveConfiguration) {
3212
3352
  return void 0;
3213
3353
  }
3214
- const configuredRepo = await this.resolveConfiguration(input.configKey);
3215
- if (typeof configuredRepo !== "string" || configuredRepo.trim().length === 0) {
3354
+ const configuredValue = await this.resolveConfiguration(
3355
+ input.target.configKey
3356
+ );
3357
+ if (typeof configuredValue !== "string" || configuredValue.trim().length === 0) {
3216
3358
  return void 0;
3217
3359
  }
3218
- const configuredTarget = parseRepoTarget(configuredRepo);
3360
+ const configuredTarget = createCapabilityTarget(
3361
+ input.target.type,
3362
+ configuredValue
3363
+ );
3219
3364
  if (!configuredTarget) {
3220
3365
  logWarn(
3221
3366
  "config_value_invalid_for_capability_target",
3222
3367
  {},
3223
3368
  {
3224
3369
  "app.skill.name": activeSkill?.name,
3225
- "app.config.key": input.configKey
3370
+ "app.config.key": input.target.configKey
3226
3371
  },
3227
- `Configured ${input.configKey} is invalid for capability target resolution`
3372
+ `Configured ${input.target.configKey} is invalid for capability target resolution`
3228
3373
  );
3229
3374
  return void 0;
3230
3375
  }
3231
3376
  const declaredConfig = activeSkill?.usesConfig ?? [];
3232
- if (activeSkill && !declaredConfig.includes(input.configKey)) {
3377
+ if (activeSkill && !declaredConfig.includes(input.target.configKey)) {
3233
3378
  logWarn(
3234
3379
  "config_key_not_declared_for_skill",
3235
3380
  {},
3236
3381
  {
3237
3382
  "app.skill.name": activeSkill.name,
3238
- "app.config.key": input.configKey
3383
+ "app.config.key": input.target.configKey
3239
3384
  },
3240
3385
  "Configuration key used by runtime is not declared in active skill frontmatter (soft enforcement)"
3241
3386
  );
@@ -3243,9 +3388,7 @@ var SkillCapabilityRuntime = class {
3243
3388
  return configuredTarget;
3244
3389
  }
3245
3390
  capabilityCacheKey(capability, target) {
3246
- const owner = target?.owner?.trim().toLowerCase();
3247
- const repo = target?.repo?.trim().toLowerCase();
3248
- const scope = owner && repo ? `${owner}/${repo}` : "none";
3391
+ const scope = target ? `${target.type}:${target.value.trim()}` : "none";
3249
3392
  return `${capability}:${scope}`;
3250
3393
  }
3251
3394
  async issueCapabilityLease(input) {
@@ -3256,10 +3399,10 @@ var SkillCapabilityRuntime = class {
3256
3399
  );
3257
3400
  }
3258
3401
  const activeSkill = input.activeSkill;
3259
- const target = capabilityProvider.target?.type === "repo" ? await this.resolveCapabilityTarget({
3402
+ const target = capabilityProvider.target ? await this.resolveCapabilityTarget({
3260
3403
  activeSkill,
3261
- repoRef: input.repoRef,
3262
- configKey: capabilityProvider.target.configKey
3404
+ target: capabilityProvider.target,
3405
+ targetRef: input.targetRef
3263
3406
  }) : void 0;
3264
3407
  return await this.router.issue({
3265
3408
  capability: input.capability,
@@ -3294,14 +3437,14 @@ var SkillCapabilityRuntime = class {
3294
3437
  );
3295
3438
  }
3296
3439
  const activeSkill = input.activeSkill;
3297
- const capabilityTarget = capabilityProvider.target?.type === "repo" ? await this.resolveCapabilityTarget({
3440
+ const capabilityTarget = capabilityProvider.target ? await this.resolveCapabilityTarget({
3298
3441
  activeSkill,
3299
- repoRef: input.repoRef,
3300
- configKey: capabilityProvider.target.configKey
3442
+ target: capabilityProvider.target,
3443
+ targetRef: input.targetRef
3301
3444
  }) : void 0;
3302
- if (capabilityProvider.target?.type === "repo" && (!capabilityTarget?.owner || !capabilityTarget?.repo)) {
3445
+ if (capabilityProvider.target && !capabilityTarget?.value.trim()) {
3303
3446
  throw new Error(
3304
- "jr-rpc issue-credential requires repository context; use --repo <owner/repo>"
3447
+ `jr-rpc issue-credential requires ${capabilityProvider.target.type} target context; use --target <value>`
3305
3448
  );
3306
3449
  }
3307
3450
  const declared = activeSkill?.requiresCapabilities ?? [];
@@ -3338,7 +3481,7 @@ var SkillCapabilityRuntime = class {
3338
3481
  const lease = await this.issueCapabilityLease({
3339
3482
  activeSkill,
3340
3483
  capability,
3341
- repoRef: input.repoRef,
3484
+ targetRef: input.targetRef,
3342
3485
  reason: input.reason
3343
3486
  });
3344
3487
  const transforms = this.toHeaderTransforms(lease);
@@ -3465,7 +3608,7 @@ var TestCredentialBroker = class {
3465
3608
  expiresAt,
3466
3609
  metadata: {
3467
3610
  reason: input.reason,
3468
- target: input.target?.owner && input.target?.repo ? `${input.target.owner}/${input.target.repo}` : "none"
3611
+ target: input.target ? `${input.target.type}:${input.target.value}` : "none"
3469
3612
  }
3470
3613
  };
3471
3614
  }
@@ -3568,24 +3711,24 @@ async function handleIssueCredentialCommand(args, deps) {
3568
3711
  exitCode: 2
3569
3712
  });
3570
3713
  }
3571
- let repoRef;
3714
+ let targetRef;
3572
3715
  const extras = args.slice(1);
3573
3716
  if (extras.length > 0) {
3574
- if (extras.length === 2 && extras[0] === "--repo") {
3575
- repoRef = extras[1];
3576
- } else if (extras.length === 1 && extras[0].startsWith("--repo=")) {
3577
- repoRef = extras[0].slice("--repo=".length);
3717
+ if (extras.length === 2 && extras[0] === "--target") {
3718
+ targetRef = extras[1]?.trim();
3719
+ } else if (extras.length === 1 && extras[0].startsWith("--target=")) {
3720
+ targetRef = extras[0].slice("--target=".length).trim();
3578
3721
  } else {
3579
3722
  return {
3580
3723
  stdout: "",
3581
- stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --repo <owner/repo>\n",
3724
+ stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --target <value>\n",
3582
3725
  exitCode: 2
3583
3726
  };
3584
3727
  }
3585
- if (!parseRepoTarget(repoRef ?? "")) {
3728
+ if (!targetRef) {
3586
3729
  return {
3587
3730
  stdout: "",
3588
- stderr: "jr-rpc issue-credential --repo must be in owner/repo format\n",
3731
+ stderr: "jr-rpc issue-credential --target requires a non-empty value\n",
3589
3732
  exitCode: 2
3590
3733
  };
3591
3734
  }
@@ -3595,7 +3738,7 @@ async function handleIssueCredentialCommand(args, deps) {
3595
3738
  outcome = await deps.capabilityRuntime.enableCapabilityForTurn({
3596
3739
  activeSkill: deps.activeSkill,
3597
3740
  capability,
3598
- ...repoRef ? { repoRef } : {},
3741
+ ...targetRef ? { targetRef } : {},
3599
3742
  reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
3600
3743
  });
3601
3744
  } catch (error) {
@@ -3985,7 +4128,7 @@ async function handleDeleteTokenCommand(args, deps) {
3985
4128
  function createJrRpcCommand(deps) {
3986
4129
  return defineCommand("jr-rpc", async (args) => {
3987
4130
  const usage = [
3988
- "jr-rpc issue-credential <capability> [--repo <owner/repo>]",
4131
+ "jr-rpc issue-credential <capability> [--target <value>]",
3989
4132
  "jr-rpc oauth-start <provider>",
3990
4133
  "jr-rpc delete-token <provider>",
3991
4134
  "jr-rpc config get <key>",
@@ -5778,7 +5921,7 @@ function createSlackCanvasCreateTool(context, state) {
5778
5921
  channelId: targetChannelId
5779
5922
  });
5780
5923
  state.setTurnCreatedCanvasId(created.canvasId);
5781
- state.patchArtifactState({
5924
+ await state.patchArtifactState({
5782
5925
  lastCanvasId: created.canvasId,
5783
5926
  lastCanvasUrl: created.permalink,
5784
5927
  recentCanvases: mergeRecentCanvases(
@@ -5877,7 +6020,7 @@ function createSlackCanvasUpdateTool(state, _context) {
5877
6020
  operation: resolvedOperation,
5878
6021
  sectionId
5879
6022
  });
5880
- state.patchArtifactState({ lastCanvasId: targetCanvasId });
6023
+ await state.patchArtifactState({ lastCanvasId: targetCanvasId });
5881
6024
  const response = {
5882
6025
  ok: true,
5883
6026
  canvas_id: targetCanvasId,
@@ -6082,7 +6225,7 @@ function createSlackListCreateTool(state) {
6082
6225
  };
6083
6226
  }
6084
6227
  const list = await createTodoList(name);
6085
- state.patchArtifactState({
6228
+ await state.patchArtifactState({
6086
6229
  lastListId: list.listId,
6087
6230
  lastListUrl: list.permalink,
6088
6231
  listColumnMap: list.listColumnMap
@@ -6145,7 +6288,7 @@ function createSlackListAddItemsTool(state) {
6145
6288
  assigneeUserId: assignee_user_id,
6146
6289
  dueDate: due_date
6147
6290
  });
6148
- state.patchArtifactState({
6291
+ await state.patchArtifactState({
6149
6292
  lastListId: targetListId,
6150
6293
  listColumnMap: result.listColumnMap
6151
6294
  });
@@ -6237,7 +6380,7 @@ function createSlackListUpdateItemTool(state) {
6237
6380
  title,
6238
6381
  listColumnMap: state.artifactState.listColumnMap ?? {}
6239
6382
  });
6240
- state.patchArtifactState({ lastListId: targetListId });
6383
+ await state.patchArtifactState({ lastListId: targetListId });
6241
6384
  const response = {
6242
6385
  ok: true,
6243
6386
  list_id: targetListId,
@@ -6838,7 +6981,7 @@ function createToolState(hooks, context) {
6838
6981
  ...context.artifactState?.listColumnMap ?? {}
6839
6982
  }
6840
6983
  };
6841
- const patchArtifactState = (patch) => {
6984
+ const patchArtifactState = async (patch) => {
6842
6985
  Object.assign(artifactState, patch);
6843
6986
  if (patch.listColumnMap) {
6844
6987
  artifactState.listColumnMap = {
@@ -6846,7 +6989,7 @@ function createToolState(hooks, context) {
6846
6989
  ...patch.listColumnMap
6847
6990
  };
6848
6991
  }
6849
- hooks.onArtifactStatePatch?.(patch);
6992
+ await hooks.onArtifactStatePatch?.(patch);
6850
6993
  };
6851
6994
  return {
6852
6995
  artifactState,
@@ -7955,10 +8098,14 @@ function createSandboxSessionManager(options) {
7955
8098
  sandboxIdHint = void 0;
7956
8099
  toolExecutors = void 0;
7957
8100
  };
7958
- const rememberSandbox = (nextSandbox) => {
8101
+ const rememberSandbox = async (nextSandbox) => {
7959
8102
  sandbox = nextSandbox;
7960
8103
  sandboxIdHint = nextSandbox.sandboxId;
7961
8104
  toolExecutors = void 0;
8105
+ await options?.onSandboxAcquired?.({
8106
+ sandboxId: nextSandbox.sandboxId,
8107
+ ...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
8108
+ });
7962
8109
  return nextSandbox;
7963
8110
  };
7964
8111
  const failSetup = (error) => {
@@ -8148,7 +8295,7 @@ function createSandboxSessionManager(options) {
8148
8295
  } catch (error) {
8149
8296
  return failSetup(error);
8150
8297
  }
8151
- return rememberSandbox(createdSandbox);
8298
+ return await rememberSandbox(createdSandbox);
8152
8299
  };
8153
8300
  const discardHintIfProfileChanged = () => {
8154
8301
  if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
@@ -8203,7 +8350,7 @@ function createSandboxSessionManager(options) {
8203
8350
  }
8204
8351
  try {
8205
8352
  await syncSkills(hintedSandbox);
8206
- return rememberSandbox(hintedSandbox);
8353
+ return await rememberSandbox(hintedSandbox);
8207
8354
  } catch (error) {
8208
8355
  if (isSandboxUnavailableError(error)) {
8209
8356
  return await recreateUnavailableSandbox("id_hint");
@@ -8450,7 +8597,8 @@ function createSandboxExecutor(options) {
8450
8597
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8451
8598
  timeoutMs: options?.timeoutMs,
8452
8599
  traceContext,
8453
- onStatus: options?.onStatus
8600
+ onStatus: options?.onStatus,
8601
+ onSandboxAcquired: options?.onSandboxAcquired
8454
8602
  });
8455
8603
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8456
8604
  const logSandboxBootRequest = (trigger, details = {}) => {
@@ -8964,66 +9112,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8964
9112
  }));
8965
9113
  }
8966
9114
 
8967
- // src/chat/runtime/turn.ts
8968
- function buildDeterministicTurnId(messageId) {
8969
- const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
8970
- return `turn_${sanitized}`;
8971
- }
8972
- var RetryableTurnError = class extends Error {
8973
- code = "retryable_turn";
8974
- reason;
8975
- constructor(reason, message) {
8976
- super(message);
8977
- this.name = "RetryableTurnError";
8978
- this.reason = reason;
8979
- }
8980
- };
8981
- function isRetryableTurnError(error, reason) {
8982
- if (!(error instanceof RetryableTurnError)) {
8983
- return false;
8984
- }
8985
- if (!reason) {
8986
- return true;
8987
- }
8988
- return error.reason === reason;
8989
- }
8990
- function startActiveTurn(args) {
8991
- args.conversation.processing.activeTurnId = args.nextTurnId;
8992
- args.updateConversationStats(args.conversation);
8993
- }
8994
- function markTurnCompleted(args) {
8995
- args.conversation.processing.activeTurnId = void 0;
8996
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
8997
- args.updateConversationStats(args.conversation);
8998
- }
8999
- function markTurnFailed(args) {
9000
- args.conversation.processing.activeTurnId = void 0;
9001
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
9002
- args.markConversationMessage(args.conversation, args.userMessageId, {
9003
- replied: false,
9004
- skippedReason: "reply failed"
9005
- });
9006
- args.updateConversationStats(args.conversation);
9007
- }
9008
- function resolveReplyDelivery(args) {
9009
- const replyHasFiles = Boolean(
9010
- args.reply.files && args.reply.files.length > 0
9011
- );
9012
- const deliveryPlan = args.reply.deliveryPlan ?? {
9013
- mode: args.reply.deliveryMode ?? "thread",
9014
- postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
9015
- attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
9016
- };
9017
- let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
9018
- if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
9019
- attachFiles = "inline";
9020
- }
9021
- return {
9022
- shouldPostThreadReply: deliveryPlan.postThreadText,
9023
- attachFiles
9024
- };
9025
- }
9026
-
9027
9115
  // src/chat/services/reply-delivery-plan.ts
9028
9116
  var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
9029
9117
  var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
@@ -9372,6 +9460,50 @@ async function persistAuthPauseCheckpoint(args) {
9372
9460
  }
9373
9461
  return nextSliceId;
9374
9462
  }
9463
+ async function persistTimeoutCheckpoint(args) {
9464
+ const nextSliceId = args.currentSliceId + 1;
9465
+ try {
9466
+ const latestCheckpoint = await getAgentTurnSessionCheckpoint(
9467
+ args.conversationId,
9468
+ args.sessionId
9469
+ );
9470
+ const piMessages = trimTrailingAssistantMessages(
9471
+ args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
9472
+ );
9473
+ return await upsertAgentTurnSessionCheckpoint({
9474
+ conversationId: args.conversationId,
9475
+ sessionId: args.sessionId,
9476
+ sliceId: nextSliceId,
9477
+ state: "awaiting_resume",
9478
+ piMessages,
9479
+ loadedSkillNames: args.loadedSkillNames,
9480
+ resumeReason: "timeout",
9481
+ resumedFromSliceId: args.currentSliceId,
9482
+ errorMessage: args.errorMessage
9483
+ });
9484
+ } catch (checkpointError) {
9485
+ logException(
9486
+ checkpointError,
9487
+ "agent_turn_timeout_resume_checkpoint_failed",
9488
+ {
9489
+ slackThreadId: args.logContext.threadId,
9490
+ slackUserId: args.logContext.requesterId,
9491
+ slackChannelId: args.logContext.channelId,
9492
+ runId: args.logContext.runId,
9493
+ assistantUserName: args.logContext.assistantUserName,
9494
+ modelId: args.logContext.modelId
9495
+ },
9496
+ {
9497
+ "app.ai.resume_conversation_id": args.conversationId,
9498
+ "app.ai.resume_session_id": args.sessionId,
9499
+ "app.ai.resume_from_slice_id": args.currentSliceId,
9500
+ "app.ai.resume_next_slice_id": nextSliceId
9501
+ },
9502
+ "Failed to persist timeout checkpoint before scheduling resume"
9503
+ );
9504
+ return void 0;
9505
+ }
9506
+ }
9375
9507
 
9376
9508
  // src/chat/services/mcp-auth-orchestration.ts
9377
9509
  var McpAuthorizationPauseError = class extends Error {
@@ -9462,21 +9594,6 @@ function mcpToolsToDefinitions(mcpTools) {
9462
9594
  }
9463
9595
  return defs;
9464
9596
  }
9465
- async function maybeReplaceAgentMessages(agent, messages) {
9466
- const resumable = agent;
9467
- if (typeof resumable.replaceMessages !== "function") {
9468
- return false;
9469
- }
9470
- await resumable.replaceMessages(messages);
9471
- return true;
9472
- }
9473
- async function runAgentContinuation(agent) {
9474
- const resumable = agent;
9475
- if (typeof resumable.continue !== "function") {
9476
- throw new Error("Agent continuation is unavailable in this runtime");
9477
- }
9478
- return await resumable.continue();
9479
- }
9480
9597
  async function generateAssistantReply(messageText, context = {}) {
9481
9598
  let timeoutResumeConversationId;
9482
9599
  let timeoutResumeSessionId;
@@ -9487,6 +9604,7 @@ async function generateAssistantReply(messageText, context = {}) {
9487
9604
  let loadedSkillNamesForResume = [];
9488
9605
  let mcpToolManager;
9489
9606
  let sandboxExecutor;
9607
+ let timedOut = false;
9490
9608
  const getSandboxMetadata = () => sandboxExecutor ? {
9491
9609
  sandboxId: sandboxExecutor.getSandboxId(),
9492
9610
  sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
@@ -9529,9 +9647,8 @@ async function generateAssistantReply(messageText, context = {}) {
9529
9647
  "Discovered startup SOUL/skills/plugins"
9530
9648
  );
9531
9649
  }
9532
- const configurationValues = {
9533
- ...context.configuration ?? {}
9534
- };
9650
+ let baseInstructions = "";
9651
+ let configurationValues;
9535
9652
  const userInput = messageText;
9536
9653
  if (shouldTrace) {
9537
9654
  logInfo(
@@ -9560,6 +9677,11 @@ async function generateAssistantReply(messageText, context = {}) {
9560
9677
  timeoutResumeConversationId = sessionConversationId;
9561
9678
  timeoutResumeSessionId = sessionId;
9562
9679
  timeoutResumeSliceId = currentSliceId;
9680
+ const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
9681
+ configurationValues = {
9682
+ ...context.configuration ?? {},
9683
+ ...persistedConfigurationValues
9684
+ };
9563
9685
  const capabilityRuntime = createSkillCapabilityRuntime({
9564
9686
  invocationArgs: skillInvocation?.args,
9565
9687
  requesterId: context.requester?.userId,
@@ -9571,6 +9693,11 @@ async function generateAssistantReply(messageText, context = {}) {
9571
9693
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
9572
9694
  traceContext: spanContext,
9573
9695
  onStatus: context.onStatus,
9696
+ onSandboxAcquired: async (sandbox2) => {
9697
+ lastKnownSandboxId = sandbox2.sandboxId;
9698
+ lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
9699
+ await context.onSandboxAcquired?.(sandbox2);
9700
+ },
9574
9701
  runBashCustomCommand: async (command) => {
9575
9702
  const result = await maybeExecuteJrRpcCustomCommand(command, {
9576
9703
  capabilityRuntime,
@@ -9710,8 +9837,14 @@ async function generateAssistantReply(messageText, context = {}) {
9710
9837
  onGeneratedFiles: (files) => {
9711
9838
  replyFiles.push(...files);
9712
9839
  },
9713
- onArtifactStatePatch: (patch) => {
9840
+ onArtifactStatePatch: async (patch) => {
9714
9841
  Object.assign(artifactStatePatch, patch);
9842
+ await context.onArtifactStateUpdated?.(
9843
+ mergeArtifactsState(
9844
+ context.artifactState ?? {},
9845
+ artifactStatePatch
9846
+ )
9847
+ );
9715
9848
  },
9716
9849
  toolOverrides: context.toolOverrides,
9717
9850
  onSkillLoaded: async (loadedSkill) => {
@@ -9761,7 +9894,7 @@ async function generateAssistantReply(messageText, context = {}) {
9761
9894
  }
9762
9895
  syncResumeState();
9763
9896
  const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
9764
- const baseInstructions = buildSystemPrompt({
9897
+ baseInstructions = buildSystemPrompt({
9765
9898
  availableSkills,
9766
9899
  activeSkills,
9767
9900
  activeTools: activeToolSummaries,
@@ -9889,15 +10022,7 @@ async function generateAssistantReply(messageText, context = {}) {
9889
10022
  let completedAssistantTurn = false;
9890
10023
  try {
9891
10024
  if (resumedFromCheckpoint) {
9892
- const didReplace = await maybeReplaceAgentMessages(
9893
- agent,
9894
- existingCheckpoint.piMessages
9895
- );
9896
- if (!didReplace) {
9897
- throw new Error(
9898
- "Agent session resume requested but replaceMessages is unavailable"
9899
- );
9900
- }
10025
+ agent.replaceMessages(existingCheckpoint.piMessages);
9901
10026
  }
9902
10027
  beforeMessageCount = agent.state.messages.length;
9903
10028
  await withSpan(
@@ -9906,16 +10031,15 @@ async function generateAssistantReply(messageText, context = {}) {
9906
10031
  spanContext,
9907
10032
  async () => {
9908
10033
  let promptResult;
9909
- const promptPromise = resumedFromCheckpoint ? runAgentContinuation(agent) : agent.prompt({
10034
+ const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
9910
10035
  role: "user",
9911
10036
  content: userContentParts,
9912
10037
  timestamp: Date.now()
9913
10038
  });
9914
10039
  let timeoutId;
9915
- let didTimeout = false;
9916
10040
  const timeoutPromise = new Promise((_, reject) => {
9917
10041
  timeoutId = setTimeout(() => {
9918
- didTimeout = true;
10042
+ timedOut = true;
9919
10043
  agent.abort();
9920
10044
  reject(
9921
10045
  new Error(
@@ -9927,7 +10051,7 @@ async function generateAssistantReply(messageText, context = {}) {
9927
10051
  try {
9928
10052
  promptResult = await Promise.race([promptPromise, timeoutPromise]);
9929
10053
  } catch (error) {
9930
- if (didTimeout) {
10054
+ if (timedOut) {
9931
10055
  logWarn(
9932
10056
  "agent_turn_timeout",
9933
10057
  {},
@@ -9953,9 +10077,7 @@ async function generateAssistantReply(messageText, context = {}) {
9953
10077
  clearTimeout(timeoutId);
9954
10078
  }
9955
10079
  }
9956
- newMessages = agent.state.messages.slice(
9957
- beforeMessageCount
9958
- );
10080
+ newMessages = agent.state.messages.slice(beforeMessageCount);
9959
10081
  completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
9960
10082
  if (mcpAuth.getPendingPause() && !completedAssistantTurn) {
9961
10083
  timeoutResumeMessages = [...agent.state.messages];
@@ -10011,6 +10133,36 @@ async function generateAssistantReply(messageText, context = {}) {
10011
10133
  assistantUserName: context.assistant?.userName
10012
10134
  });
10013
10135
  } catch (error) {
10136
+ if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
10137
+ const checkpoint = await persistTimeoutCheckpoint({
10138
+ conversationId: timeoutResumeConversationId,
10139
+ sessionId: timeoutResumeSessionId,
10140
+ currentSliceId: timeoutResumeSliceId,
10141
+ messages: timeoutResumeMessages,
10142
+ loadedSkillNames: loadedSkillNamesForResume,
10143
+ errorMessage: error instanceof Error ? error.message : String(error),
10144
+ logContext: {
10145
+ threadId: context.correlation?.threadId,
10146
+ requesterId: context.correlation?.requesterId,
10147
+ channelId: context.correlation?.channelId,
10148
+ runId: context.correlation?.runId,
10149
+ assistantUserName: context.assistant?.userName,
10150
+ modelId: botConfig.modelId
10151
+ }
10152
+ });
10153
+ if (checkpoint) {
10154
+ throw new RetryableTurnError(
10155
+ "turn_timeout_resume",
10156
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId} version=${checkpoint.checkpointVersion}`,
10157
+ {
10158
+ conversationId: timeoutResumeConversationId,
10159
+ sessionId: timeoutResumeSessionId,
10160
+ sliceId: checkpoint.sliceId,
10161
+ checkpointVersion: checkpoint.checkpointVersion
10162
+ }
10163
+ );
10164
+ }
10165
+ }
10014
10166
  if (error instanceof McpAuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
10015
10167
  const nextSliceId = await persistAuthPauseCheckpoint({
10016
10168
  conversationId: timeoutResumeConversationId,
@@ -10030,7 +10182,12 @@ async function generateAssistantReply(messageText, context = {}) {
10030
10182
  });
10031
10183
  throw new RetryableTurnError(
10032
10184
  "mcp_auth_resume",
10033
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
10185
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
10186
+ {
10187
+ conversationId: timeoutResumeConversationId,
10188
+ sessionId: timeoutResumeSessionId,
10189
+ sliceId: nextSliceId
10190
+ }
10034
10191
  );
10035
10192
  }
10036
10193
  if (isRetryableTurnError(error)) {
@@ -10241,12 +10398,15 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10241
10398
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10242
10399
  }
10243
10400
  async function postSlackMessage(channelId, threadTs, text) {
10401
+ await getSlackClient().chat.postMessage({
10402
+ channel: channelId,
10403
+ thread_ts: threadTs,
10404
+ text
10405
+ });
10406
+ }
10407
+ async function postSlackMessageBestEffort(channelId, threadTs, text) {
10244
10408
  try {
10245
- await getSlackClient().chat.postMessage({
10246
- channel: channelId,
10247
- thread_ts: threadTs,
10248
- text
10249
- });
10409
+ await postSlackMessage(channelId, threadTs, text);
10250
10410
  } catch {
10251
10411
  }
10252
10412
  }
@@ -10276,32 +10436,85 @@ function createReadOnlyConfigService(values) {
10276
10436
  }
10277
10437
  };
10278
10438
  }
10279
- async function resumeAuthorizedRequest(args) {
10280
- const progress = createProgressReporter({
10281
- channelId: args.channelId,
10282
- threadTs: args.threadTs,
10283
- transport: createSlackWebApiAssistantStatusTransport()
10439
+ var ResumeTurnBusyError = class extends Error {
10440
+ constructor(lockKey) {
10441
+ super(`A turn already owns resume lock "${lockKey}"`);
10442
+ this.name = "ResumeTurnBusyError";
10443
+ }
10444
+ };
10445
+ function getDefaultLockKey(channelId, threadTs) {
10446
+ return `slack:${channelId}:${threadTs}`;
10447
+ }
10448
+ function createResumeReplyContext(args, progress) {
10449
+ const replyContext = args.replyContext ?? {};
10450
+ const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10451
+ const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
10452
+ return {
10453
+ ...replyContext,
10454
+ assistant: {
10455
+ userName: botConfig.userName,
10456
+ ...replyContext.assistant
10457
+ },
10458
+ correlation: {
10459
+ ...replyContext.correlation,
10460
+ threadId: replyContext.correlation?.threadId ?? threadId,
10461
+ channelId: replyContext.correlation?.channelId ?? args.channelId,
10462
+ threadTs: replyContext.correlation?.threadTs ?? args.threadTs,
10463
+ requesterId: replyContext.correlation?.requesterId ?? replyContext.requester?.userId
10464
+ },
10465
+ channelConfiguration: persistedChannelConfiguration,
10466
+ onSandboxAcquired: async (sandbox) => {
10467
+ await persistThreadStateById(threadId, {
10468
+ sandboxId: sandbox.sandboxId,
10469
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
10470
+ });
10471
+ await replyContext.onSandboxAcquired?.(sandbox);
10472
+ },
10473
+ onArtifactStateUpdated: async (artifacts) => {
10474
+ await persistThreadStateById(threadId, { artifacts });
10475
+ await replyContext.onArtifactStateUpdated?.(artifacts);
10476
+ },
10477
+ onStatus: async (status) => {
10478
+ await progress.setStatus(status);
10479
+ await replyContext.onStatus?.(status);
10480
+ }
10481
+ };
10482
+ }
10483
+ async function resumeSlackTurn(args) {
10484
+ const requesterUserId = args.replyContext?.requester?.userId;
10485
+ if (!requesterUserId) {
10486
+ throw new Error("Resumed turn requires replyContext.requester.userId");
10487
+ }
10488
+ const stateAdapter = getStateAdapter();
10489
+ await stateAdapter.connect();
10490
+ const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10491
+ const lock = await stateAdapter.acquireLock(
10492
+ lockKey,
10493
+ botConfig.turnTimeoutMs + 6e4
10494
+ );
10495
+ if (!lock) {
10496
+ throw new ResumeTurnBusyError(lockKey);
10497
+ }
10498
+ const progress = createProgressReporter({
10499
+ channelId: args.channelId,
10500
+ threadTs: args.threadTs,
10501
+ transport: createSlackWebApiAssistantStatusTransport()
10284
10502
  });
10285
- await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
10286
- await progress.start();
10503
+ let deferredPauseHandler;
10504
+ let deferredFailureHandler;
10287
10505
  try {
10506
+ if (args.initialText) {
10507
+ await postSlackMessageBestEffort(
10508
+ args.channelId,
10509
+ args.threadTs,
10510
+ args.initialText
10511
+ );
10512
+ }
10513
+ await progress.start();
10288
10514
  const generateReply = args.generateReply ?? generateAssistantReply;
10515
+ const replyContext = createResumeReplyContext(args, progress);
10289
10516
  const replyPromise = generateReply(args.messageText, {
10290
- assistant: { userName: botConfig.userName },
10291
- requester: { userId: args.requesterUserId },
10292
- correlation: {
10293
- conversationId: args.correlation?.conversationId,
10294
- turnId: args.correlation?.turnId,
10295
- channelId: args.correlation?.channelId ?? args.channelId,
10296
- threadTs: args.correlation?.threadTs ?? args.threadTs,
10297
- requesterId: args.correlation?.requesterId ?? args.requesterUserId
10298
- },
10299
- toolChannelId: args.toolChannelId,
10300
- conversationContext: args.conversationContext,
10301
- artifactState: args.artifactState,
10302
- configuration: args.configuration,
10303
- channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
10304
- onStatus: (status) => progress.setStatus(status)
10517
+ ...replyContext
10305
10518
  });
10306
10519
  const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
10307
10520
  const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
@@ -10327,11 +10540,168 @@ async function resumeAuthorizedRequest(args) {
10327
10540
  } catch (error) {
10328
10541
  await progress.stop();
10329
10542
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
10330
- await args.onAuthPause(error);
10543
+ deferredPauseHandler = async () => {
10544
+ await args.onAuthPause?.(error);
10545
+ };
10546
+ } else if (isRetryableTurnError(error, "turn_timeout_resume") && args.onTimeoutPause) {
10547
+ deferredPauseHandler = async () => {
10548
+ await args.onTimeoutPause?.(error);
10549
+ };
10550
+ } else {
10551
+ deferredFailureHandler = async () => {
10552
+ await args.onFailure?.(error);
10553
+ if (args.failureText) {
10554
+ await postSlackMessageBestEffort(
10555
+ args.channelId,
10556
+ args.threadTs,
10557
+ args.failureText
10558
+ );
10559
+ }
10560
+ };
10561
+ }
10562
+ } finally {
10563
+ await stateAdapter.releaseLock(lock);
10564
+ }
10565
+ if (deferredPauseHandler) {
10566
+ try {
10567
+ await deferredPauseHandler();
10568
+ return;
10569
+ } catch (pauseError) {
10570
+ await args.onFailure?.(pauseError);
10571
+ if (args.failureText) {
10572
+ await postSlackMessageBestEffort(
10573
+ args.channelId,
10574
+ args.threadTs,
10575
+ args.failureText
10576
+ );
10577
+ }
10331
10578
  return;
10332
10579
  }
10333
- await args.onFailure?.(error);
10334
- await postSlackMessage(args.channelId, args.threadTs, args.failureText);
10580
+ }
10581
+ if (deferredFailureHandler) {
10582
+ await deferredFailureHandler();
10583
+ }
10584
+ }
10585
+ async function resumeAuthorizedRequest(args) {
10586
+ await resumeSlackTurn({
10587
+ messageText: args.messageText,
10588
+ channelId: args.channelId,
10589
+ threadTs: args.threadTs,
10590
+ replyContext: args.replyContext,
10591
+ lockKey: args.lockKey,
10592
+ initialText: args.connectedText,
10593
+ failureText: args.failureText,
10594
+ generateReply: args.generateReply,
10595
+ onReply: args.onReply,
10596
+ onSuccess: args.onSuccess,
10597
+ onFailure: args.onFailure,
10598
+ onAuthPause: args.onAuthPause,
10599
+ onTimeoutPause: args.onTimeoutPause,
10600
+ replyTimeoutMs: args.replyTimeoutMs
10601
+ });
10602
+ }
10603
+
10604
+ // src/chat/services/timeout-resume.ts
10605
+ import { createHmac, timingSafeEqual } from "crypto";
10606
+ var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
10607
+ var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
10608
+ var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
10609
+ var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
10610
+ var TURN_TIMEOUT_RESUME_SIGNATURE_HEADER = "x-junior-resume-signature";
10611
+ var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
10612
+ function canScheduleTurnTimeoutResume(nextSliceId) {
10613
+ return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
10614
+ }
10615
+ function getTurnTimeoutResumeSecret() {
10616
+ const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
10617
+ if (explicit) {
10618
+ return explicit;
10619
+ }
10620
+ return getSlackSigningSecret();
10621
+ }
10622
+ function buildSignedPayload(timestamp, body) {
10623
+ return `${timestamp}:${body}`;
10624
+ }
10625
+ function signTurnTimeoutResumeBody(secret, timestamp, body) {
10626
+ const digest = createHmac("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
10627
+ return `${TURN_TIMEOUT_RESUME_SIGNATURE_VERSION}=${digest}`;
10628
+ }
10629
+ function timingSafeMatch(expected, actual) {
10630
+ const expectedBuffer = Buffer.from(expected);
10631
+ const actualBuffer = Buffer.from(actual);
10632
+ if (expectedBuffer.length !== actualBuffer.length) {
10633
+ return false;
10634
+ }
10635
+ return timingSafeEqual(expectedBuffer, actualBuffer);
10636
+ }
10637
+ function parseTurnTimeoutResumeRequest(value) {
10638
+ if (!value || typeof value !== "object") {
10639
+ return void 0;
10640
+ }
10641
+ const record = value;
10642
+ if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof record.expectedCheckpointVersion !== "number") {
10643
+ return void 0;
10644
+ }
10645
+ return {
10646
+ conversationId: record.conversationId,
10647
+ sessionId: record.sessionId,
10648
+ expectedCheckpointVersion: record.expectedCheckpointVersion
10649
+ };
10650
+ }
10651
+ async function scheduleTurnTimeoutResume(request) {
10652
+ const baseUrl = resolveBaseUrl();
10653
+ if (!baseUrl) {
10654
+ throw new Error(
10655
+ "Cannot determine base URL for timeout resume callback (set JUNIOR_BASE_URL or deploy to Vercel)"
10656
+ );
10657
+ }
10658
+ const secret = getTurnTimeoutResumeSecret();
10659
+ if (!secret) {
10660
+ throw new Error(
10661
+ "Cannot determine timeout resume secret (set JUNIOR_INTERNAL_RESUME_SECRET or SLACK_SIGNING_SECRET)"
10662
+ );
10663
+ }
10664
+ const body = JSON.stringify(request);
10665
+ const timestamp = Date.now().toString();
10666
+ const response = await fetch(`${baseUrl}${TURN_TIMEOUT_RESUME_PATH}`, {
10667
+ method: "POST",
10668
+ headers: {
10669
+ "content-type": "application/json",
10670
+ [TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER]: timestamp,
10671
+ [TURN_TIMEOUT_RESUME_SIGNATURE_HEADER]: signTurnTimeoutResumeBody(
10672
+ secret,
10673
+ timestamp,
10674
+ body
10675
+ )
10676
+ },
10677
+ body
10678
+ });
10679
+ if (!response.ok) {
10680
+ throw new Error(
10681
+ `Timeout resume callback failed with status ${response.status}`
10682
+ );
10683
+ }
10684
+ }
10685
+ async function verifyTurnTimeoutResumeRequest(request) {
10686
+ const timestamp = request.headers.get(TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER)?.trim() ?? "";
10687
+ const signature = request.headers.get(TURN_TIMEOUT_RESUME_SIGNATURE_HEADER)?.trim() ?? "";
10688
+ const secret = getTurnTimeoutResumeSecret();
10689
+ if (!timestamp || !signature || !secret) {
10690
+ return void 0;
10691
+ }
10692
+ const parsedTimestamp = Number.parseInt(timestamp, 10);
10693
+ if (!Number.isFinite(parsedTimestamp) || Math.abs(Date.now() - parsedTimestamp) > TURN_TIMEOUT_RESUME_MAX_SKEW_MS) {
10694
+ return void 0;
10695
+ }
10696
+ const body = await request.text();
10697
+ const expectedSignature = signTurnTimeoutResumeBody(secret, timestamp, body);
10698
+ if (!timingSafeMatch(expectedSignature, signature)) {
10699
+ return void 0;
10700
+ }
10701
+ try {
10702
+ return parseTurnTimeoutResumeRequest(JSON.parse(body));
10703
+ } catch {
10704
+ return void 0;
10335
10705
  }
10336
10706
  }
10337
10707
 
@@ -10445,28 +10815,12 @@ async function deliverReplyToThread(channelId, threadTs, reply) {
10445
10815
  } catch {
10446
10816
  }
10447
10817
  }
10448
- function buildDeterministicTurnId2(messageId) {
10449
- const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
10450
- return `turn_${sanitized}`;
10451
- }
10452
- function getUserMessageIdForTurn(conversation, sessionId) {
10453
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
10454
- const message = conversation.messages[index];
10455
- if (message?.role !== "user") {
10456
- continue;
10457
- }
10458
- if (buildDeterministicTurnId2(message.id) === sessionId) {
10459
- return message.id;
10460
- }
10461
- }
10462
- return void 0;
10463
- }
10464
10818
  async function buildResumeConversationContext(channelId, threadTs, sessionId) {
10465
10819
  const threadId = `slack:${channelId}:${threadTs}`;
10466
10820
  const conversation = coerceThreadConversationState(
10467
10821
  await getPersistedThreadState(threadId)
10468
10822
  );
10469
- const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
10823
+ const userMessageId = getTurnUserMessageId(conversation, sessionId);
10470
10824
  return buildConversationContext(conversation, {
10471
10825
  excludeMessageId: userMessageId
10472
10826
  });
@@ -10477,7 +10831,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
10477
10831
  const conversation = coerceThreadConversationState(currentState);
10478
10832
  const artifacts = coerceThreadArtifactsState(currentState);
10479
10833
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
10480
- const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
10834
+ const userMessageId = getTurnUserMessageId(conversation, sessionId);
10481
10835
  markConversationMessage(conversation, userMessageId, {
10482
10836
  replied: true,
10483
10837
  skippedReason: void 0
@@ -10514,7 +10868,7 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
10514
10868
  markTurnFailed({
10515
10869
  conversation,
10516
10870
  nowMs: Date.now(),
10517
- userMessageId: getUserMessageIdForTurn(conversation, sessionId),
10871
+ userMessageId: getTurnUserMessageId(conversation, sessionId),
10518
10872
  markConversationMessage,
10519
10873
  updateConversationStats
10520
10874
  });
@@ -10527,6 +10881,17 @@ async function resumeAuthorizedMcpTurn(args) {
10527
10881
  if (!authSession.channelId || !authSession.threadTs) {
10528
10882
  return;
10529
10883
  }
10884
+ const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
10885
+ const currentState = await getPersistedThreadState(threadId);
10886
+ const conversation = coerceThreadConversationState(currentState);
10887
+ const artifacts = coerceThreadArtifactsState(currentState);
10888
+ const userMessage = getTurnUserMessage(conversation, authSession.sessionId);
10889
+ if (conversation.processing.activeTurnId !== authSession.sessionId) {
10890
+ return;
10891
+ }
10892
+ const channelConfiguration = getChannelConfigurationServiceById(
10893
+ authSession.channelId
10894
+ );
10530
10895
  const conversationContext = await buildResumeConversationContext(
10531
10896
  authSession.channelId,
10532
10897
  authSession.threadTs,
@@ -10534,23 +10899,34 @@ async function resumeAuthorizedMcpTurn(args) {
10534
10899
  );
10535
10900
  await resumeAuthorizedRequest({
10536
10901
  messageText: authSession.userMessage,
10537
- requesterUserId: authSession.userId,
10538
10902
  provider,
10539
10903
  channelId: authSession.channelId,
10540
10904
  threadTs: authSession.threadTs,
10905
+ lockKey: authSession.conversationId,
10541
10906
  connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
10542
10907
  failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
10543
- correlation: {
10544
- conversationId: authSession.conversationId,
10545
- turnId: authSession.sessionId,
10546
- channelId: authSession.channelId,
10547
- threadTs: authSession.threadTs,
10548
- requesterId: authSession.userId
10908
+ replyContext: {
10909
+ assistant: { userName: botConfig.userName },
10910
+ requester: {
10911
+ userId: authSession.userId,
10912
+ userName: userMessage?.author?.userName,
10913
+ fullName: userMessage?.author?.fullName
10914
+ },
10915
+ correlation: {
10916
+ conversationId: authSession.conversationId,
10917
+ turnId: authSession.sessionId,
10918
+ channelId: authSession.channelId,
10919
+ threadTs: authSession.threadTs,
10920
+ requesterId: authSession.userId
10921
+ },
10922
+ toolChannelId: authSession.toolChannelId ?? artifacts.assistantContextChannelId ?? authSession.channelId,
10923
+ conversationContext,
10924
+ artifactState: artifacts,
10925
+ configuration: authSession.configuration,
10926
+ channelConfiguration,
10927
+ sandbox: getPersistedSandboxState(currentState),
10928
+ threadParticipants: buildThreadParticipants(conversation.messages)
10549
10929
  },
10550
- toolChannelId: authSession.toolChannelId ?? authSession.artifactState?.assistantContextChannelId ?? authSession.channelId,
10551
- conversationContext,
10552
- artifactState: authSession.artifactState,
10553
- configuration: authSession.configuration,
10554
10930
  onReply: async (reply) => {
10555
10931
  await deliverReplyToThread(
10556
10932
  authSession.channelId,
@@ -10607,6 +10983,37 @@ async function resumeAuthorizedMcpTurn(args) {
10607
10983
  { "app.credential.provider": provider },
10608
10984
  "Resumed MCP turn requested another authorization flow"
10609
10985
  );
10986
+ },
10987
+ onTimeoutPause: async (error) => {
10988
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
10989
+ throw error;
10990
+ }
10991
+ const checkpointVersion = error.metadata?.checkpointVersion;
10992
+ const nextSliceId = error.metadata?.sliceId;
10993
+ if (typeof checkpointVersion !== "number") {
10994
+ throw new Error(
10995
+ "Timed-out MCP resume did not include a checkpoint version"
10996
+ );
10997
+ }
10998
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
10999
+ logWarn(
11000
+ "mcp_oauth_callback_resume_slice_limit_reached",
11001
+ {},
11002
+ {
11003
+ "app.credential.provider": provider,
11004
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
11005
+ },
11006
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
11007
+ );
11008
+ throw new Error(
11009
+ "Timed-out turn exceeded the automatic resume slice limit"
11010
+ );
11011
+ }
11012
+ await scheduleTurnTimeoutResume({
11013
+ conversationId: authSession.conversationId,
11014
+ sessionId: authSession.sessionId,
11015
+ expectedCheckpointVersion: checkpointVersion
11016
+ });
10610
11017
  }
10611
11018
  });
10612
11019
  }
@@ -10823,14 +11230,16 @@ async function resumePendingOAuthMessage(stored) {
10823
11230
  );
10824
11231
  await resumeAuthorizedRequest({
10825
11232
  messageText: stored.pendingMessage,
10826
- requesterUserId: stored.userId,
10827
11233
  provider: stored.provider,
10828
11234
  channelId: stored.channelId,
10829
11235
  threadTs: stored.threadTs,
10830
11236
  connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
10831
11237
  failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
10832
- conversationContext,
10833
- configuration: stored.configuration,
11238
+ replyContext: {
11239
+ requester: { userId: stored.userId },
11240
+ conversationContext,
11241
+ configuration: stored.configuration
11242
+ },
10834
11243
  onSuccess: async (reply) => {
10835
11244
  logInfo(
10836
11245
  "oauth_callback_resume_complete",
@@ -11019,6 +11428,344 @@ async function GET5(request, provider, waitUntil) {
11019
11428
  });
11020
11429
  }
11021
11430
 
11431
+ // src/handlers/turn-resume.ts
11432
+ import { Buffer as Buffer3 } from "buffer";
11433
+
11434
+ // src/chat/slack/context.ts
11435
+ function toTrimmedSlackString(value) {
11436
+ const normalized = toOptionalString(value);
11437
+ return normalized?.trim() || void 0;
11438
+ }
11439
+ function parseSlackThreadId(threadId) {
11440
+ const normalizedThreadId = toTrimmedSlackString(threadId);
11441
+ if (!normalizedThreadId) {
11442
+ return void 0;
11443
+ }
11444
+ const parts = normalizedThreadId.split(":");
11445
+ if (parts.length !== 3 || parts[0] !== "slack") {
11446
+ return void 0;
11447
+ }
11448
+ const channelId = toTrimmedSlackString(parts[1]);
11449
+ const threadTs = toTrimmedSlackString(parts[2]);
11450
+ if (!channelId || !threadTs) {
11451
+ return void 0;
11452
+ }
11453
+ return { channelId, threadTs };
11454
+ }
11455
+ function resolveSlackChannelIdFromThreadId(threadId) {
11456
+ return parseSlackThreadId(threadId)?.channelId;
11457
+ }
11458
+ function resolveSlackChannelIdFromMessage(message) {
11459
+ const messageChannelId = toTrimmedSlackString(
11460
+ message.channelId
11461
+ );
11462
+ if (messageChannelId) {
11463
+ return messageChannelId;
11464
+ }
11465
+ const raw = message.raw;
11466
+ if (raw && typeof raw === "object") {
11467
+ const rawChannel = toTrimmedSlackString(
11468
+ raw.channel
11469
+ );
11470
+ if (rawChannel) {
11471
+ return rawChannel;
11472
+ }
11473
+ }
11474
+ const threadId = toTrimmedSlackString(
11475
+ message.threadId
11476
+ );
11477
+ return resolveSlackChannelIdFromThreadId(threadId);
11478
+ }
11479
+
11480
+ // src/handlers/turn-resume.ts
11481
+ function extractSlackText2(text, files) {
11482
+ const message = buildSlackOutputMessage(text, files);
11483
+ if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
11484
+ return message.markdown;
11485
+ }
11486
+ if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
11487
+ return message.raw;
11488
+ }
11489
+ return text;
11490
+ }
11491
+ async function normalizeFileUploads2(files) {
11492
+ const normalized = [];
11493
+ for (const file of files) {
11494
+ let data;
11495
+ if (Buffer3.isBuffer(file.data)) {
11496
+ data = file.data;
11497
+ } else if (file.data instanceof ArrayBuffer) {
11498
+ data = Buffer3.from(file.data);
11499
+ } else {
11500
+ data = Buffer3.from(await file.data.arrayBuffer());
11501
+ }
11502
+ normalized.push({
11503
+ data,
11504
+ filename: file.filename
11505
+ });
11506
+ }
11507
+ return normalized;
11508
+ }
11509
+ async function deliverReplyToThread2(args) {
11510
+ const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
11511
+ const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
11512
+ reply: args.reply,
11513
+ hasStreamedThreadReply: false
11514
+ });
11515
+ if (shouldPostThreadReply) {
11516
+ const text = extractSlackText2(
11517
+ args.reply.text,
11518
+ attachFiles === "inline" ? replyFiles : void 0
11519
+ );
11520
+ if (text.trim().length > 0) {
11521
+ await postSlackMessage(args.channelId, args.threadTs, text);
11522
+ }
11523
+ }
11524
+ if (!replyFiles || attachFiles === "none") {
11525
+ return;
11526
+ }
11527
+ const files = await normalizeFileUploads2(replyFiles);
11528
+ if (files.length === 0) {
11529
+ return;
11530
+ }
11531
+ try {
11532
+ await uploadFilesToThread({
11533
+ channelId: args.channelId,
11534
+ threadTs: args.threadTs,
11535
+ files
11536
+ });
11537
+ } catch {
11538
+ }
11539
+ }
11540
+ async function persistCompletedReplyState2(args) {
11541
+ const currentState = await getPersistedThreadState(
11542
+ args.checkpoint.conversationId
11543
+ );
11544
+ const conversation = coerceThreadConversationState(currentState);
11545
+ const artifacts = coerceThreadArtifactsState(currentState);
11546
+ const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
11547
+ const userMessage = getTurnUserMessage(
11548
+ conversation,
11549
+ args.checkpoint.sessionId
11550
+ );
11551
+ markConversationMessage(conversation, userMessage?.id, {
11552
+ replied: true,
11553
+ skippedReason: void 0
11554
+ });
11555
+ upsertConversationMessage(conversation, {
11556
+ id: generateConversationId("assistant"),
11557
+ role: "assistant",
11558
+ text: normalizeConversationText(args.reply.text) || "[empty response]",
11559
+ createdAtMs: Date.now(),
11560
+ author: {
11561
+ userName: botConfig.userName,
11562
+ isBot: true
11563
+ },
11564
+ meta: {
11565
+ replied: true
11566
+ }
11567
+ });
11568
+ markTurnCompleted({
11569
+ conversation,
11570
+ nowMs: Date.now(),
11571
+ updateConversationStats
11572
+ });
11573
+ await persistThreadStateById(args.checkpoint.conversationId, {
11574
+ artifacts: nextArtifacts,
11575
+ conversation,
11576
+ sandboxId: args.reply.sandboxId,
11577
+ sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
11578
+ });
11579
+ }
11580
+ async function persistFailedReplyState2(checkpoint) {
11581
+ const currentState = await getPersistedThreadState(checkpoint.conversationId);
11582
+ const conversation = coerceThreadConversationState(currentState);
11583
+ markTurnFailed({
11584
+ conversation,
11585
+ nowMs: Date.now(),
11586
+ userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
11587
+ markConversationMessage,
11588
+ updateConversationStats
11589
+ });
11590
+ await persistThreadStateById(checkpoint.conversationId, {
11591
+ conversation
11592
+ });
11593
+ }
11594
+ async function resumeTimedOutTurn(payload) {
11595
+ const checkpoint = await getAgentTurnSessionCheckpoint(
11596
+ payload.conversationId,
11597
+ payload.sessionId
11598
+ );
11599
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
11600
+ return;
11601
+ }
11602
+ const thread = parseSlackThreadId(payload.conversationId);
11603
+ if (!thread) {
11604
+ throw new Error(
11605
+ `Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
11606
+ );
11607
+ }
11608
+ const currentState = await getPersistedThreadState(payload.conversationId);
11609
+ const conversation = coerceThreadConversationState(currentState);
11610
+ const artifacts = coerceThreadArtifactsState(currentState);
11611
+ const userMessage = getTurnUserMessage(conversation, payload.sessionId);
11612
+ if (!userMessage?.author?.userId) {
11613
+ throw new Error(
11614
+ `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
11615
+ );
11616
+ }
11617
+ if (conversation.processing.activeTurnId !== payload.sessionId) {
11618
+ return;
11619
+ }
11620
+ const channelConfiguration = getChannelConfigurationServiceById(
11621
+ thread.channelId
11622
+ );
11623
+ const conversationContext = buildConversationContext(conversation, {
11624
+ excludeMessageId: userMessage.id
11625
+ });
11626
+ const sandbox = getPersistedSandboxState(currentState);
11627
+ await resumeSlackTurn({
11628
+ messageText: userMessage.text,
11629
+ channelId: thread.channelId,
11630
+ threadTs: thread.threadTs,
11631
+ lockKey: payload.conversationId,
11632
+ failureText: "I hit an error while resuming that request. Please try the command again.",
11633
+ replyContext: {
11634
+ assistant: { userName: botConfig.userName },
11635
+ requester: {
11636
+ userId: userMessage.author.userId,
11637
+ userName: userMessage.author.userName,
11638
+ fullName: userMessage.author.fullName
11639
+ },
11640
+ correlation: {
11641
+ conversationId: payload.conversationId,
11642
+ turnId: payload.sessionId,
11643
+ channelId: thread.channelId,
11644
+ threadTs: thread.threadTs,
11645
+ requesterId: userMessage.author.userId
11646
+ },
11647
+ toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
11648
+ artifactState: artifacts,
11649
+ conversationContext,
11650
+ channelConfiguration,
11651
+ sandbox,
11652
+ threadParticipants: buildThreadParticipants(conversation.messages)
11653
+ },
11654
+ onReply: async (reply) => {
11655
+ await deliverReplyToThread2({
11656
+ channelId: thread.channelId,
11657
+ threadTs: thread.threadTs,
11658
+ reply
11659
+ });
11660
+ },
11661
+ onSuccess: async (reply) => {
11662
+ try {
11663
+ await persistCompletedReplyState2({ checkpoint, reply });
11664
+ } catch (persistError) {
11665
+ logException(
11666
+ persistError,
11667
+ "timeout_resume_complete_persist_failed",
11668
+ {},
11669
+ {
11670
+ "app.ai.conversation_id": payload.conversationId,
11671
+ "app.ai.session_id": payload.sessionId
11672
+ },
11673
+ "Failed to persist completed timeout-resume state after reply delivery"
11674
+ );
11675
+ }
11676
+ },
11677
+ onFailure: async (error) => {
11678
+ logException(
11679
+ error,
11680
+ "timeout_resume_failed",
11681
+ {},
11682
+ {
11683
+ "app.ai.conversation_id": payload.conversationId,
11684
+ "app.ai.session_id": payload.sessionId
11685
+ },
11686
+ "Failed to resume timed-out turn"
11687
+ );
11688
+ await persistFailedReplyState2(checkpoint);
11689
+ },
11690
+ onAuthPause: async () => {
11691
+ logWarn(
11692
+ "timeout_resume_reparked_for_auth",
11693
+ {},
11694
+ {
11695
+ "app.ai.conversation_id": payload.conversationId,
11696
+ "app.ai.session_id": payload.sessionId
11697
+ },
11698
+ "Resumed timed-out turn parked for auth"
11699
+ );
11700
+ },
11701
+ onTimeoutPause: async (error) => {
11702
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
11703
+ throw error;
11704
+ }
11705
+ const checkpointVersion = error.metadata?.checkpointVersion;
11706
+ const nextSliceId = error.metadata?.sliceId;
11707
+ if (typeof checkpointVersion !== "number") {
11708
+ throw new Error(
11709
+ "Timed-out resume turn did not include a checkpoint version"
11710
+ );
11711
+ }
11712
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
11713
+ logWarn(
11714
+ "timeout_resume_slice_limit_reached",
11715
+ {},
11716
+ {
11717
+ "app.ai.conversation_id": payload.conversationId,
11718
+ "app.ai.session_id": payload.sessionId,
11719
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
11720
+ },
11721
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
11722
+ );
11723
+ throw new Error(
11724
+ "Timed-out turn exceeded the automatic resume slice limit"
11725
+ );
11726
+ }
11727
+ await scheduleTurnTimeoutResume({
11728
+ conversationId: payload.conversationId,
11729
+ sessionId: payload.sessionId,
11730
+ expectedCheckpointVersion: checkpointVersion
11731
+ });
11732
+ }
11733
+ });
11734
+ }
11735
+ async function POST(request, waitUntil) {
11736
+ const payload = await verifyTurnTimeoutResumeRequest(request);
11737
+ if (!payload) {
11738
+ return new Response("Unauthorized", { status: 401 });
11739
+ }
11740
+ waitUntil(
11741
+ () => resumeTimedOutTurn(payload).catch((error) => {
11742
+ if (error instanceof ResumeTurnBusyError) {
11743
+ logWarn(
11744
+ "timeout_resume_lock_busy",
11745
+ {},
11746
+ {
11747
+ "app.ai.conversation_id": payload.conversationId,
11748
+ "app.ai.session_id": payload.sessionId
11749
+ },
11750
+ "Skipped timeout resume because another turn owns the thread lock"
11751
+ );
11752
+ return;
11753
+ }
11754
+ logException(
11755
+ error,
11756
+ "timeout_resume_handler_failed",
11757
+ {},
11758
+ {
11759
+ "app.ai.conversation_id": payload.conversationId,
11760
+ "app.ai.session_id": payload.sessionId
11761
+ },
11762
+ "Timeout resume handler failed"
11763
+ );
11764
+ })
11765
+ );
11766
+ return new Response("Accepted", { status: 202 });
11767
+ }
11768
+
11022
11769
  // src/chat/app/production.ts
11023
11770
  import { createSlackAdapter } from "@chat-adapter/slack";
11024
11771
 
@@ -11048,11 +11795,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
11048
11795
  var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
11049
11796
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
11050
11797
  var RECENT_THREAD_WINDOW = 6;
11051
- function escapeRegExp(value) {
11798
+ function escapeRegExp2(value) {
11052
11799
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11053
11800
  }
11054
11801
  function containsAssistantInvocation(text, botUserName) {
11055
- const escapedUserName = escapeRegExp(botUserName);
11802
+ const escapedUserName = escapeRegExp2(botUserName);
11056
11803
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
11057
11804
  const labeledEntityMentionRe = new RegExp(
11058
11805
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -11340,54 +12087,8 @@ async function decideSubscribedThreadReply(args) {
11340
12087
  }
11341
12088
  }
11342
12089
 
11343
- // src/chat/slack/context.ts
11344
- function toTrimmedSlackString(value) {
11345
- const normalized = toOptionalString(value);
11346
- return normalized?.trim() || void 0;
11347
- }
11348
- function parseSlackThreadId(threadId) {
11349
- const normalizedThreadId = toTrimmedSlackString(threadId);
11350
- if (!normalizedThreadId) {
11351
- return void 0;
11352
- }
11353
- const parts = normalizedThreadId.split(":");
11354
- if (parts.length !== 3 || parts[0] !== "slack") {
11355
- return void 0;
11356
- }
11357
- const channelId = toTrimmedSlackString(parts[1]);
11358
- const threadTs = toTrimmedSlackString(parts[2]);
11359
- if (!channelId || !threadTs) {
11360
- return void 0;
11361
- }
11362
- return { channelId, threadTs };
11363
- }
11364
- function resolveSlackChannelIdFromThreadId(threadId) {
11365
- return parseSlackThreadId(threadId)?.channelId;
11366
- }
11367
- function resolveSlackChannelIdFromMessage(message) {
11368
- const messageChannelId = toTrimmedSlackString(
11369
- message.channelId
11370
- );
11371
- if (messageChannelId) {
11372
- return messageChannelId;
11373
- }
11374
- const raw = message.raw;
11375
- if (raw && typeof raw === "object") {
11376
- const rawChannel = toTrimmedSlackString(
11377
- raw.channel
11378
- );
11379
- if (rawChannel) {
11380
- return rawChannel;
11381
- }
11382
- }
11383
- const threadId = toTrimmedSlackString(
11384
- message.threadId
11385
- );
11386
- return resolveSlackChannelIdFromThreadId(threadId);
11387
- }
11388
-
11389
12090
  // src/chat/runtime/thread-context.ts
11390
- function escapeRegExp2(value) {
12091
+ function escapeRegExp3(value) {
11391
12092
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11392
12093
  }
11393
12094
  function stripLeadingBotMention(text, options = {}) {
@@ -11397,12 +12098,12 @@ function stripLeadingBotMention(text, options = {}) {
11397
12098
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
11398
12099
  }
11399
12100
  const mentionByNameRe = new RegExp(
11400
- `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
12101
+ `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
11401
12102
  "i"
11402
12103
  );
11403
12104
  next = next.replace(mentionByNameRe, "").trim();
11404
12105
  const mentionByLabeledEntityRe = new RegExp(
11405
- `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
12106
+ `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
11406
12107
  "i"
11407
12108
  );
11408
12109
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -12430,6 +13131,7 @@ function createJuniorRuntimeServices(overrides = {}) {
12430
13131
  replyExecutor: {
12431
13132
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
12432
13133
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
13134
+ scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
12433
13135
  generateThreadTitle: conversationMemory.generateThreadTitle
12434
13136
  },
12435
13137
  subscribedReplyPolicy: createSubscribedReplyPolicy({
@@ -12494,19 +13196,6 @@ function getExecutionFailureReason(reply) {
12494
13196
  }
12495
13197
  return "empty assistant turn";
12496
13198
  }
12497
- function buildParticipants(messages) {
12498
- const seen = /* @__PURE__ */ new Set();
12499
- const participants = [];
12500
- for (const message of messages) {
12501
- const { userId, userName, fullName } = message.author ?? {};
12502
- if (!userId || message.author?.isBot) continue;
12503
- if (!seen.has(userId)) {
12504
- seen.add(userId);
12505
- participants.push({ userId, userName, fullName });
12506
- }
12507
- }
12508
- return participants;
12509
- }
12510
13199
  function createReplyToThread(deps) {
12511
13200
  return async function replyToThread(thread, message, options = {}) {
12512
13201
  if (message.author.isMe) {
@@ -12666,7 +13355,7 @@ function createReplyToThread(deps) {
12666
13355
  let shouldPersistFailureState = true;
12667
13356
  try {
12668
13357
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
12669
- const threadParticipants = buildParticipants(
13358
+ const threadParticipants = buildThreadParticipants(
12670
13359
  preparedState.conversation.messages
12671
13360
  );
12672
13361
  const reply = await deps.services.generateAssistantReply(userText, {
@@ -12698,6 +13387,15 @@ function createReplyToThread(deps) {
12698
13387
  sandboxId: preparedState.sandboxId,
12699
13388
  sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
12700
13389
  },
13390
+ onSandboxAcquired: async (sandbox) => {
13391
+ await persistThreadState(thread, {
13392
+ sandboxId: sandbox.sandboxId,
13393
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
13394
+ });
13395
+ },
13396
+ onArtifactStateUpdated: async (artifacts) => {
13397
+ await persistThreadState(thread, { artifacts });
13398
+ },
12701
13399
  threadParticipants,
12702
13400
  onStatus: (status) => progress.setStatus(status),
12703
13401
  onTextDelta: (deltaText) => {
@@ -12894,10 +13592,66 @@ function createReplyToThread(deps) {
12894
13592
  );
12895
13593
  }
12896
13594
  } catch (error) {
12897
- shouldPersistFailureState = !isRetryableTurnError(
12898
- error,
12899
- "mcp_auth_resume"
12900
- );
13595
+ if (isRetryableTurnError(error, "mcp_auth_resume")) {
13596
+ shouldPersistFailureState = false;
13597
+ throw error;
13598
+ }
13599
+ if (isRetryableTurnError(error, "turn_timeout_resume")) {
13600
+ textStream.end();
13601
+ const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
13602
+ if (hasVisibleAssistantOutput) {
13603
+ logWarn(
13604
+ "agent_turn_timeout_resume_skipped_after_visible_output",
13605
+ turnTraceContext,
13606
+ messageTs ? { "messaging.message.id": messageTs } : {},
13607
+ "Skipped automatic timeout resume because assistant text had already started streaming"
13608
+ );
13609
+ }
13610
+ const conversationIdForResume = error.metadata?.conversationId;
13611
+ const sessionIdForResume = error.metadata?.sessionId;
13612
+ const checkpointVersion = error.metadata?.checkpointVersion;
13613
+ const nextSliceId = error.metadata?.sliceId;
13614
+ if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
13615
+ try {
13616
+ await deps.services.scheduleTurnTimeoutResume({
13617
+ conversationId: conversationIdForResume,
13618
+ sessionId: sessionIdForResume,
13619
+ expectedCheckpointVersion: checkpointVersion
13620
+ });
13621
+ shouldPersistFailureState = false;
13622
+ return;
13623
+ } catch (scheduleError) {
13624
+ logException(
13625
+ scheduleError,
13626
+ "agent_turn_timeout_resume_schedule_failed",
13627
+ turnTraceContext,
13628
+ {
13629
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
13630
+ "app.ai.resume_checkpoint_version": checkpointVersion
13631
+ },
13632
+ "Failed to schedule timeout resume callback"
13633
+ );
13634
+ }
13635
+ } else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
13636
+ logWarn(
13637
+ "agent_turn_timeout_resume_slice_limit_reached",
13638
+ turnTraceContext,
13639
+ {
13640
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
13641
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
13642
+ },
13643
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
13644
+ );
13645
+ } else {
13646
+ logWarn(
13647
+ "agent_turn_timeout_resume_metadata_missing",
13648
+ turnTraceContext,
13649
+ messageTs ? { "messaging.message.id": messageTs } : {},
13650
+ "Timed-out turn could not be scheduled for resume because retry metadata was incomplete"
13651
+ );
13652
+ }
13653
+ }
13654
+ shouldPersistFailureState = true;
12901
13655
  throw error;
12902
13656
  } finally {
12903
13657
  textStream.end();
@@ -13493,8 +14247,10 @@ async function handleSlashCommand(event) {
13493
14247
  var productionBot;
13494
14248
  var productionSlackRuntime;
13495
14249
  function createProductionBot() {
14250
+ const logger = createChatSdkLogger();
13496
14251
  return new JuniorChat({
13497
14252
  userName: botConfig.userName,
14253
+ logger,
13498
14254
  concurrency: {
13499
14255
  strategy: "queue",
13500
14256
  // The SDK's default queueEntryTtlMs is 90s, but Junior turns can
@@ -13514,6 +14270,7 @@ function createProductionBot() {
13514
14270
  throw new Error("SLACK_SIGNING_SECRET is required");
13515
14271
  }
13516
14272
  return createSlackAdapter({
14273
+ logger: logger.child("slack"),
13517
14274
  signingSecret,
13518
14275
  ...botToken ? { botToken } : {},
13519
14276
  ...clientId ? { clientId } : {},
@@ -13660,11 +14417,13 @@ function extractMessageChangedMention(body, botUserId, adapter) {
13660
14417
  const threadTs = event.message.thread_ts ?? messageTs;
13661
14418
  const userId = event.message.user ?? "unknown";
13662
14419
  const threadId = `slack:${channelId}:${threadTs}`;
14420
+ const teamId = typeof body.team_id === "string" ? body.team_id : void 0;
13663
14421
  const raw = {
13664
14422
  channel: channelId,
13665
14423
  ts: messageTs,
13666
14424
  thread_ts: threadTs,
13667
- user: userId
14425
+ user: userId,
14426
+ ...teamId ? { team_id: teamId } : {}
13668
14427
  };
13669
14428
  const message = new Message({
13670
14429
  id: getEditedMentionMessageId(messageTs),
@@ -13845,7 +14604,7 @@ async function handlePlatformWebhook(request, platform, waitUntil, bot = getProd
13845
14604
  }
13846
14605
  });
13847
14606
  }
13848
- async function POST(request, platform, waitUntil) {
14607
+ async function POST2(request, platform, waitUntil) {
13849
14608
  return handlePlatformWebhook(request, platform, waitUntil);
13850
14609
  }
13851
14610
 
@@ -13898,8 +14657,11 @@ async function createApp(options) {
13898
14657
  app.get("/api/oauth/callback/:provider", (c) => {
13899
14658
  return GET5(c.req.raw, c.req.param("provider"), waitUntil);
13900
14659
  });
14660
+ app.post("/api/internal/turn-resume", (c) => {
14661
+ return POST(c.req.raw, waitUntil);
14662
+ });
13901
14663
  app.post("/api/webhooks/:platform", (c) => {
13902
- return POST(c.req.raw, c.req.param("platform"), waitUntil);
14664
+ return POST2(c.req.raw, c.req.param("platform"), waitUntil);
13903
14665
  });
13904
14666
  return app;
13905
14667
  }