@sentry/junior 0.24.1 → 0.25.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
@@ -1,12 +1,10 @@
1
1
  import {
2
2
  discoverSkills,
3
3
  findSkillByName,
4
- getCapabilityProvider,
5
- listCapabilityProviders,
6
4
  loadSkillsByName,
7
5
  logCapabilityCatalogLoadedOnce,
8
6
  parseSkillInvocation
9
- } from "./chunk-O5N42P7K.js";
7
+ } from "./chunk-ICIRAL6Y.js";
10
8
  import {
11
9
  SANDBOX_DATA_ROOT,
12
10
  SANDBOX_SKILLS_ROOT,
@@ -27,7 +25,7 @@ import {
27
25
  sandboxSkillDir,
28
26
  sandboxSkillFile,
29
27
  toOptionalTrimmed
30
- } from "./chunk-RMVXZMXQ.js";
28
+ } from "./chunk-A75TWGF2.js";
31
29
  import {
32
30
  CredentialUnavailableError,
33
31
  buildOAuthTokenRequest,
@@ -59,7 +57,7 @@ import {
59
57
  toOptionalString,
60
58
  withContext,
61
59
  withSpan
62
- } from "./chunk-I3WA75AD.js";
60
+ } from "./chunk-RZJDO55D.js";
63
61
  import "./chunk-Z3YD6NHK.js";
64
62
  import {
65
63
  discoverInstalledPluginPackageContent,
@@ -1219,234 +1217,6 @@ async function addReactionToMessage(input) {
1219
1217
  return { ok: true };
1220
1218
  }
1221
1219
 
1222
- // src/chat/respond-helpers.ts
1223
- var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
1224
- function getSessionIdentifiers(context) {
1225
- return {
1226
- conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
1227
- sessionId: context.correlation?.turnId
1228
- };
1229
- }
1230
- function isExecutionDeferralResponse(text) {
1231
- return /\b(want me to proceed|do you want me to proceed|shall i proceed|can i proceed|should i proceed|let me do that now|give me a moment|tag me again|fresh invocation)\b/i.test(
1232
- text
1233
- );
1234
- }
1235
- function isToolAccessDisclaimerResponse(text) {
1236
- return /\b(i (don't|do not) have access to (active )?tool|tool results came back empty|prior results .* empty|cannot access .*tool|need to (run|load) .*tool .* first)\b/i.test(
1237
- text
1238
- );
1239
- }
1240
- function isExecutionEscapeResponse(text) {
1241
- const trimmed = text.trim();
1242
- if (!trimmed) return false;
1243
- return isExecutionDeferralResponse(trimmed) || isToolAccessDisclaimerResponse(trimmed);
1244
- }
1245
- function parseJsonCandidate(text) {
1246
- const trimmed = text.trim();
1247
- if (!trimmed) return void 0;
1248
- try {
1249
- return JSON.parse(trimmed);
1250
- } catch {
1251
- const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
1252
- if (!fenced) return void 0;
1253
- try {
1254
- return JSON.parse(fenced[1]);
1255
- } catch {
1256
- return void 0;
1257
- }
1258
- }
1259
- }
1260
- function isToolPayloadShape(payload) {
1261
- if (!payload || typeof payload !== "object") return false;
1262
- const record = payload;
1263
- const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
1264
- if (type.startsWith("tool-")) return true;
1265
- if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
1266
- return true;
1267
- const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
1268
- const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
1269
- if (hasToolName && hasToolInput) return true;
1270
- return false;
1271
- }
1272
- function isRawToolPayloadResponse(text) {
1273
- const parsed = parseJsonCandidate(text);
1274
- if (Array.isArray(parsed)) {
1275
- return parsed.some((entry) => isToolPayloadShape(entry));
1276
- }
1277
- if (isToolPayloadShape(parsed)) {
1278
- return true;
1279
- }
1280
- const compact = text.replace(/\s+/g, " ");
1281
- return /"type"\s*:\s*"tool[-_](use|call|result|error)"/i.test(compact);
1282
- }
1283
- function toObservablePromptPart(part) {
1284
- if (part.type === "text") {
1285
- return {
1286
- type: "text",
1287
- text: part.text
1288
- };
1289
- }
1290
- return {
1291
- type: "image",
1292
- mimeType: part.mimeType,
1293
- data: `[omitted:${part.data.length}]`
1294
- };
1295
- }
1296
- function summarizeMessageText(text) {
1297
- const normalized = text.trim().replace(/\s+/g, " ");
1298
- if (!normalized) {
1299
- return "[empty]";
1300
- }
1301
- return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
1302
- }
1303
- function buildUserTurnText(userInput, conversationContext, metadata) {
1304
- const trimmedContext = conversationContext?.trim();
1305
- const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
1306
- const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
1307
- if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
1308
- return userInput;
1309
- }
1310
- const sections = [
1311
- "<current-message>",
1312
- userInput,
1313
- "</current-message>"
1314
- ];
1315
- if (trimmedContext) {
1316
- sections.push(
1317
- "",
1318
- "<thread-conversation-context>",
1319
- "Use this context for continuity across prior thread turns.",
1320
- trimmedContext,
1321
- "</thread-conversation-context>"
1322
- );
1323
- }
1324
- if (metadata?.sessionContext?.conversationId) {
1325
- sections.push(
1326
- "",
1327
- "<session-context>",
1328
- `- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
1329
- "</session-context>"
1330
- );
1331
- }
1332
- if (metadata?.turnContext?.traceId) {
1333
- sections.push(
1334
- "",
1335
- "<turn-context>",
1336
- `- trace_id: ${metadata.turnContext.traceId}`,
1337
- "</turn-context>"
1338
- );
1339
- }
1340
- return sections.join("\n");
1341
- }
1342
- function encodeNonImageAttachmentForPrompt(attachment) {
1343
- const base64 = attachment.data.toString("base64");
1344
- const wasTruncated = base64.length > MAX_INLINE_ATTACHMENT_BASE64_CHARS;
1345
- const encodedPayload = wasTruncated ? `${base64.slice(0, MAX_INLINE_ATTACHMENT_BASE64_CHARS)}...` : base64;
1346
- return [
1347
- "<attachment>",
1348
- `filename: ${attachment.filename ?? "unnamed"}`,
1349
- `media_type: ${attachment.mediaType}`,
1350
- "encoding: base64",
1351
- `truncated: ${wasTruncated ? "true" : "false"}`,
1352
- "<data_base64>",
1353
- encodedPayload,
1354
- "</data_base64>",
1355
- "</attachment>"
1356
- ].join("\n");
1357
- }
1358
- function buildExecutionFailureMessage(toolErrorCount) {
1359
- if (toolErrorCount > 0) {
1360
- return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
1361
- }
1362
- return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
1363
- }
1364
- function isToolResultMessage(value) {
1365
- return typeof value === "object" && value !== null && value.role === "toolResult";
1366
- }
1367
- function normalizeToolNameFromResult(result) {
1368
- if (!result || typeof result !== "object") return void 0;
1369
- const record = result;
1370
- if (typeof record.toolName === "string" && record.toolName.length > 0) {
1371
- return record.toolName;
1372
- }
1373
- if (typeof record.name === "string" && record.name.length > 0) {
1374
- return record.name;
1375
- }
1376
- return void 0;
1377
- }
1378
- function isToolResultError(result) {
1379
- if (!result || typeof result !== "object") return false;
1380
- return Boolean(result.isError);
1381
- }
1382
- function isAssistantMessage(value) {
1383
- return typeof value === "object" && value !== null && value.role === "assistant";
1384
- }
1385
- function getPiMessageRole(value) {
1386
- if (!value || typeof value !== "object") {
1387
- return void 0;
1388
- }
1389
- const role = value.role;
1390
- return typeof role === "string" ? role : void 0;
1391
- }
1392
- function extractAssistantText(message) {
1393
- const content = message.content ?? [];
1394
- return content.filter(
1395
- (part) => part.type === "text" && typeof part.text === "string"
1396
- ).map((part) => part.text).join("\n");
1397
- }
1398
- function getTerminalAssistantMessages(messages) {
1399
- let lastToolResultIndex = -1;
1400
- for (let index = messages.length - 1; index >= 0; index -= 1) {
1401
- if (isToolResultMessage(messages[index])) {
1402
- lastToolResultIndex = index;
1403
- break;
1404
- }
1405
- }
1406
- return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
1407
- }
1408
- function hasCompletedAssistantTurn(messages) {
1409
- const message = getTerminalAssistantMessages(messages).at(-1);
1410
- if (!message) {
1411
- return false;
1412
- }
1413
- const stopReason = message.stopReason;
1414
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1415
- }
1416
- function upsertActiveSkill(activeSkills, next) {
1417
- const existing = activeSkills.find((skill) => skill.name === next.name);
1418
- if (existing) {
1419
- existing.body = next.body;
1420
- existing.description = next.description;
1421
- existing.skillPath = next.skillPath;
1422
- existing.allowedTools = next.allowedTools;
1423
- existing.requiresCapabilities = next.requiresCapabilities;
1424
- existing.usesConfig = next.usesConfig;
1425
- existing.pluginProvider = next.pluginProvider;
1426
- return;
1427
- }
1428
- activeSkills.push(next);
1429
- }
1430
- function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
1431
- const keys = /* @__PURE__ */ new Set();
1432
- for (const skill of [
1433
- ...activeSkills,
1434
- ...explicitSkill ? [explicitSkill] : []
1435
- ]) {
1436
- for (const key of skill.usesConfig ?? []) {
1437
- keys.add(key);
1438
- }
1439
- }
1440
- return [...keys].sort((a, b) => a.localeCompare(b));
1441
- }
1442
- function trimTrailingAssistantMessages(messages) {
1443
- let end = messages.length;
1444
- while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
1445
- end -= 1;
1446
- }
1447
- return end === messages.length ? [...messages] : messages.slice(0, end);
1448
- }
1449
-
1450
1220
  // src/chat/oauth-flow.ts
1451
1221
  var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
1452
1222
  function formatProviderLabel(provider) {
@@ -1560,7 +1330,9 @@ async function startOAuthFlow(provider, input) {
1560
1330
  ...input.channelId ? { channelId: input.channelId } : {},
1561
1331
  ...input.threadTs ? { threadTs: input.threadTs } : {},
1562
1332
  ...input.userMessage ? { pendingMessage: input.userMessage } : {},
1563
- ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
1333
+ ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {},
1334
+ ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : {},
1335
+ ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}
1564
1336
  },
1565
1337
  OAUTH_STATE_TTL_MS
1566
1338
  );
@@ -1597,61 +1369,6 @@ async function startOAuthFlow(provider, input) {
1597
1369
  })
1598
1370
  };
1599
1371
  }
1600
- function extractOAuthStartedPayload(value) {
1601
- if (typeof value === "string") {
1602
- const parsed = parseJsonCandidate(value);
1603
- return parsed === void 0 ? void 0 : extractOAuthStartedPayload(parsed);
1604
- }
1605
- if (Array.isArray(value)) {
1606
- for (const entry of value) {
1607
- const found = extractOAuthStartedPayload(entry);
1608
- if (found) {
1609
- return found;
1610
- }
1611
- }
1612
- return void 0;
1613
- }
1614
- if (!value || typeof value !== "object") {
1615
- return void 0;
1616
- }
1617
- const record = value;
1618
- if (record.oauth_started === true) {
1619
- const message = typeof record.message === "string" ? record.message.trim() : void 0;
1620
- return message ? { message } : {};
1621
- }
1622
- const content = record.content;
1623
- if (Array.isArray(content)) {
1624
- for (const part of content) {
1625
- const text = part && typeof part === "object" && part.type === "text" && typeof part.text === "string" ? part.text : part;
1626
- const found = extractOAuthStartedPayload(text);
1627
- if (found) {
1628
- return found;
1629
- }
1630
- }
1631
- }
1632
- for (const key of ["details", "output", "result", "stdout"]) {
1633
- if (!(key in record)) {
1634
- continue;
1635
- }
1636
- const found = extractOAuthStartedPayload(record[key]);
1637
- if (found) {
1638
- return found;
1639
- }
1640
- }
1641
- return void 0;
1642
- }
1643
- function extractOAuthStartedMessageFromToolResults(toolResults) {
1644
- for (const result of toolResults) {
1645
- if (normalizeToolNameFromResult(result) !== "bash" || isToolResultError(result)) {
1646
- continue;
1647
- }
1648
- const found = extractOAuthStartedPayload(result);
1649
- if (found?.message) {
1650
- return found.message;
1651
- }
1652
- }
1653
- return void 0;
1654
- }
1655
1372
 
1656
1373
  // src/chat/mcp/oauth-provider.ts
1657
1374
  function createClientMetadata(callbackUrl) {
@@ -2375,7 +2092,7 @@ function getPiGatewayApiKeyOverride() {
2375
2092
  function extractText(message) {
2376
2093
  return (message.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text ?? "").join("").trim();
2377
2094
  }
2378
- function parseJsonCandidate2(text) {
2095
+ function parseJsonCandidate(text) {
2379
2096
  const trimmed = text.trim();
2380
2097
  if (!trimmed) return void 0;
2381
2098
  try {
@@ -2542,7 +2259,7 @@ async function completeObject(params) {
2542
2259
  );
2543
2260
  throw error;
2544
2261
  }
2545
- const candidate = parseJsonCandidate2(text);
2262
+ const candidate = parseJsonCandidate(text);
2546
2263
  const parsed = params.schema.safeParse(candidate);
2547
2264
  if (!parsed.success) {
2548
2265
  const preview = text.length > 400 ? `${text.slice(0, 400)}...` : text;
@@ -3494,6 +3211,9 @@ function formatAvailableSkillsForPrompt(skills) {
3494
3211
  ` <description>${escapeXml(skill.description)}</description>`
3495
3212
  );
3496
3213
  lines.push(` <location>${escapeXml(skillLocation)}</location>`);
3214
+ if (skill.pluginProvider) {
3215
+ lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
3216
+ }
3497
3217
  if (skill.usesConfig && skill.usesConfig.length > 0) {
3498
3218
  lines.push(
3499
3219
  ` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
@@ -3515,11 +3235,6 @@ function formatLoadedSkillsForPrompt(skills) {
3515
3235
  ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
3516
3236
  );
3517
3237
  lines.push(`References are relative to ${escapeXml(skillDir)}.`);
3518
- if (skill.requiresCapabilities && skill.requiresCapabilities.length > 0) {
3519
- lines.push(
3520
- `Requires capabilities: ${escapeXml(skill.requiresCapabilities.join(", "))}.`
3521
- );
3522
- }
3523
3238
  if (skill.usesConfig && skill.usesConfig.length > 0) {
3524
3239
  lines.push(
3525
3240
  `Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
@@ -3533,18 +3248,20 @@ function formatLoadedSkillsForPrompt(skills) {
3533
3248
  return lines.join("\n");
3534
3249
  }
3535
3250
  function formatProviderCatalogForPrompt() {
3536
- const providers = listCapabilityProviders();
3251
+ const providers = getPluginProviders().map((plugin) => plugin.manifest);
3537
3252
  if (providers.length === 0) {
3538
3253
  return "- none";
3539
3254
  }
3540
3255
  const lines = [];
3541
3256
  for (const provider of providers) {
3542
- lines.push(`- provider: ${escapeXml(provider.provider)}`);
3257
+ lines.push(`- provider: ${escapeXml(provider.name)}`);
3543
3258
  lines.push(
3544
3259
  ` - config_keys: ${provider.configKeys.length > 0 ? escapeXml(provider.configKeys.join(", ")) : "none"}`
3545
3260
  );
3546
3261
  lines.push(
3547
- ` - capabilities: ${provider.capabilities.length > 0 ? escapeXml(provider.capabilities.join(", ")) : "none"}`
3262
+ ` - default_context: ${provider.target ? escapeXml(
3263
+ `${provider.target.type} via ${provider.target.configKey}`
3264
+ ) : "none"}`
3548
3265
  );
3549
3266
  }
3550
3267
  return lines.join("\n");
@@ -3731,13 +3448,25 @@ function buildSystemPrompt(params) {
3731
3448
  ].join("\n")
3732
3449
  ),
3733
3450
  renderTag(
3734
- "provider-capabilities",
3451
+ "provider-config",
3735
3452
  [
3736
- "Use this catalog to map provider intents to valid config keys and capability names.",
3453
+ "Use this catalog to map already-chosen provider work to valid config keys and provider defaults.",
3454
+ "Do not use this catalog by itself to decide which domain skill matches an operational task.",
3737
3455
  "When user intent is to set a provider default, choose a config key from this catalog and use jr-rpc config set.",
3456
+ "The `jr-rpc` config command is a built-in bash custom command when conversation config is available; do not claim it is missing just because no `jr-rpc` skill is loaded.",
3738
3457
  formatProviderCatalogForPrompt()
3739
3458
  ].join("\n")
3740
3459
  ),
3460
+ renderTag(
3461
+ "skill-routing",
3462
+ [
3463
+ "- Choose the skill that matches the requested operation, not incidental nouns, product names, organization names, or channel context.",
3464
+ "- When multiple skills seem adjacent, prefer the one whose description matches the user's requested action most directly.",
3465
+ "- If the task needs evidence from a specialized external system or workflow, load the matching skill before drawing conclusions.",
3466
+ "- The provider-config catalog is for exact config keys and provider defaults after skill selection. It is not a shortcut for choosing a domain.",
3467
+ "- Never start provider auth speculatively. First load the skill that owns the operation, then let runtime-managed credential injection handle authenticated provider commands for that skill."
3468
+ ].join("\n")
3469
+ ),
3741
3470
  renderTag(
3742
3471
  "tool-usage",
3743
3472
  [
@@ -3750,6 +3479,8 @@ function buildSystemPrompt(params) {
3750
3479
  "- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
3751
3480
  "- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
3752
3481
  "- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
3482
+ "- When the user provides multiple sources, synthesize them explicitly as one combined answer instead of anchoring the reply on a single page or API call.",
3483
+ "- When the user provides explicit URLs or named sources, briefly anchor the answer to those provided sources (for example, 'Across the provided Slack docs...') so the summary reads as researched rather than generic memory.",
3753
3484
  "- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
3754
3485
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3755
3486
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
@@ -3767,14 +3498,14 @@ function buildSystemPrompt(params) {
3767
3498
  "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
3768
3499
  "- 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.",
3769
3500
  "- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
3770
- "- 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.",
3771
- "- Use the minimum declared capability needed for the current operation.",
3772
- "- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
3773
- "- 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.",
3774
- "- 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.",
3775
- "- Provider-targeted capabilities may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3501
+ "- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
3502
+ "- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
3503
+ "- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
3504
+ "- If provider authorization is required, the runtime sends the private authorization link itself and resumes the paused turn after authorization.",
3505
+ "- Do not try to manage provider auth directly. Run the real provider command and let the runtime handle authorization, reconnect, and resume behavior.",
3506
+ "- Provider-targeted commands may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3776
3507
  "- 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.",
3777
- "- Capabilities must match the exact provider-qualified tokens declared by the loaded skill or provider catalog.",
3508
+ "- `jr-rpc` config commands are built into the bash runtime for conversation-scoped config work; they do not require a separate helper binary in the sandbox.",
3778
3509
  "- When your work is complete, provide the exact user-facing markdown response.",
3779
3510
  "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3780
3511
  "- Prefer `webSearch` before `webFetch` when the user gave no URL.",
@@ -3792,6 +3523,8 @@ function buildSystemPrompt(params) {
3792
3523
  "- If an explicitly invoked skill is present in <loaded_skills>, never say the skill is unavailable, missing, or unsupported in this environment.",
3793
3524
  "- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
3794
3525
  "- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
3526
+ "- When multiple skills appear relevant, prefer the skill whose description best matches the requested action rather than incidental domain nouns in the prompt.",
3527
+ "- For explicit config tasks, you may load the helper skill that owns config commands, but do not use helper skills to choose the provider for unrelated operational work.",
3795
3528
  "- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3796
3529
  "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3797
3530
  "- Load only the best matching skill first; do not load multiple skills upfront.",
@@ -3834,74 +3567,35 @@ var ProviderCredentialRouter = class {
3834
3567
  this.brokersByProvider = input.brokersByProvider;
3835
3568
  }
3836
3569
  async issue(input) {
3837
- const provider = getCapabilityProvider(input.capability)?.provider;
3838
- if (!provider) {
3839
- throw new Error(`Unsupported capability: ${input.capability}`);
3840
- }
3841
- const broker = this.brokersByProvider[provider];
3570
+ const broker = this.brokersByProvider[input.provider];
3842
3571
  if (!broker) {
3843
- throw new Error(`No credential broker registered for provider: ${provider}`);
3572
+ throw new Error(
3573
+ `No credential broker registered for provider: ${input.provider}`
3574
+ );
3844
3575
  }
3845
- return await broker.issue(input);
3576
+ return await broker.issue({
3577
+ reason: input.reason,
3578
+ requesterId: input.requesterId
3579
+ });
3846
3580
  }
3847
3581
  };
3848
3582
 
3849
- // src/chat/capabilities/target.ts
3850
- function escapeRegExp(value) {
3851
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3852
- }
3853
- function normalizeTargetValue(value) {
3854
- let normalized = value.trim();
3855
- if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
3856
- normalized = normalized.slice(1, -1).trim();
3857
- }
3858
- return normalized || void 0;
3859
- }
3860
- function extractFlagValue(text, flags) {
3861
- if (flags.length === 0) {
3862
- return void 0;
3863
- }
3864
- const pattern = flags.map(escapeRegExp).join("|");
3865
- const match = new RegExp(
3866
- String.raw`(?:^|\s)(?:${pattern})(?:\s+|=)([^\s]+)`
3867
- ).exec(text);
3868
- return match ? normalizeTargetValue(match[1] ?? "") : void 0;
3869
- }
3870
- function createCapabilityTarget(type, value) {
3871
- const normalizedType = type.trim();
3872
- const normalizedValue = normalizeTargetValue(value);
3873
- if (!normalizedType || !normalizedValue) {
3874
- return void 0;
3875
- }
3876
- return {
3877
- type: normalizedType,
3878
- value: normalizedValue
3879
- };
3880
- }
3881
- function extractCapabilityTarget(params) {
3882
- const flags = params.target.commandFlags ?? [];
3883
- if (params.commandText) {
3884
- const value = extractFlagValue(params.commandText, flags);
3885
- if (value) {
3886
- return createCapabilityTarget(params.target.type, value);
3887
- }
3888
- }
3889
- if (params.invocationArgs) {
3890
- const value = extractFlagValue(params.invocationArgs, flags);
3891
- if (value) {
3892
- return createCapabilityTarget(params.target.type, value);
3893
- }
3583
+ // src/chat/capabilities/runtime.ts
3584
+ function toHeaderTransforms(lease) {
3585
+ if (!Array.isArray(lease.headerTransforms) || lease.headerTransforms.length === 0) {
3586
+ return [];
3894
3587
  }
3895
- return void 0;
3588
+ return lease.headerTransforms.filter(
3589
+ (transform) => Boolean(transform?.domain?.trim()) && transform.headers && typeof transform.headers === "object" && Object.keys(transform.headers).length > 0
3590
+ ).map((transform) => ({
3591
+ domain: transform.domain.trim(),
3592
+ headers: transform.headers
3593
+ }));
3896
3594
  }
3897
-
3898
- // src/chat/capabilities/runtime.ts
3899
3595
  var SkillCapabilityRuntime = class {
3900
3596
  router;
3901
- invocationArgs;
3902
3597
  requesterId;
3903
- resolveConfiguration;
3904
- enabledByCapability = /* @__PURE__ */ new Map();
3598
+ enabledByProvider = /* @__PURE__ */ new Map();
3905
3599
  constructor(params) {
3906
3600
  if (params.router) {
3907
3601
  this.router = params.router;
@@ -3914,136 +3608,21 @@ var SkillCapabilityRuntime = class {
3914
3608
  "SkillCapabilityRuntime requires either router or broker"
3915
3609
  );
3916
3610
  }
3917
- this.invocationArgs = params.invocationArgs;
3918
3611
  this.requesterId = params.requesterId;
3919
- this.resolveConfiguration = params.resolveConfiguration;
3920
3612
  }
3921
- async resolveCapabilityTarget(input) {
3922
- const activeSkill = input.activeSkill;
3923
- const explicitTarget = input.targetRef ? createCapabilityTarget(input.target.type, input.targetRef) : void 0;
3924
- if (explicitTarget) {
3925
- return explicitTarget;
3926
- }
3927
- const inferredTarget = extractCapabilityTarget({
3928
- invocationArgs: this.invocationArgs,
3929
- target: input.target
3930
- });
3931
- if (inferredTarget) {
3932
- return inferredTarget;
3933
- }
3934
- if (!this.resolveConfiguration) {
3935
- return void 0;
3936
- }
3937
- const configuredValue = await this.resolveConfiguration(
3938
- input.target.configKey
3939
- );
3940
- if (typeof configuredValue !== "string" || configuredValue.trim().length === 0) {
3941
- return void 0;
3942
- }
3943
- const configuredTarget = createCapabilityTarget(
3944
- input.target.type,
3945
- configuredValue
3946
- );
3947
- if (!configuredTarget) {
3948
- logWarn(
3949
- "config_value_invalid_for_capability_target",
3950
- {},
3951
- {
3952
- "app.skill.name": activeSkill?.name,
3953
- "app.config.key": input.target.configKey
3954
- },
3955
- `Configured ${input.target.configKey} is invalid for capability target resolution`
3956
- );
3613
+ async enableCredentialsForTurn(input) {
3614
+ const provider = input.activeSkill?.pluginProvider;
3615
+ if (!provider) {
3957
3616
  return void 0;
3958
3617
  }
3959
- const declaredConfig = activeSkill?.usesConfig ?? [];
3960
- if (activeSkill && !declaredConfig.includes(input.target.configKey)) {
3961
- logWarn(
3962
- "config_key_not_declared_for_skill",
3963
- {},
3964
- {
3965
- "app.skill.name": activeSkill.name,
3966
- "app.config.key": input.target.configKey
3967
- },
3968
- "Configuration key used by runtime is not declared in active skill frontmatter (soft enforcement)"
3969
- );
3970
- }
3971
- return configuredTarget;
3972
- }
3973
- capabilityCacheKey(capability, target) {
3974
- const scope = target ? `${target.type}:${target.value.trim()}` : "none";
3975
- return `${capability}:${scope}`;
3976
- }
3977
- async issueCapabilityLease(input) {
3978
- const capabilityProvider = getCapabilityProvider(input.capability);
3979
- if (!capabilityProvider) {
3980
- throw new Error(
3981
- `Unsupported capability for lease issuance: ${input.capability}`
3982
- );
3983
- }
3984
- const activeSkill = input.activeSkill;
3985
- const target = capabilityProvider.target ? await this.resolveCapabilityTarget({
3986
- activeSkill,
3987
- target: capabilityProvider.target,
3988
- targetRef: input.targetRef
3989
- }) : void 0;
3990
- return await this.router.issue({
3991
- capability: input.capability,
3992
- target,
3993
- reason: input.reason,
3994
- requesterId: this.requesterId
3995
- });
3996
- }
3997
- toHeaderTransforms(lease) {
3998
- if (Array.isArray(lease.headerTransforms) && lease.headerTransforms.length > 0) {
3999
- return lease.headerTransforms.filter(
4000
- (transform) => Boolean(transform?.domain?.trim()) && transform.headers && typeof transform.headers === "object" && Object.keys(transform.headers).length > 0
4001
- ).map((transform) => ({
4002
- domain: transform.domain.trim(),
4003
- headers: transform.headers
4004
- }));
4005
- }
4006
- return [];
4007
- }
4008
- async enableCapabilityForTurn(input) {
4009
3618
  if (!this.requesterId) {
4010
- throw new Error("jr-rpc issue-credential requires requester context");
4011
- }
4012
- const capability = input.capability.trim();
4013
- if (!capability) {
4014
- throw new Error("jr-rpc issue-credential requires a capability argument");
4015
- }
4016
- const capabilityProvider = getCapabilityProvider(capability);
4017
- if (!capabilityProvider) {
4018
- throw new Error(
4019
- `Unsupported capability for jr-rpc issue-credential: ${capability}`
4020
- );
4021
- }
4022
- const activeSkill = input.activeSkill;
4023
- const capabilityTarget = capabilityProvider.target ? await this.resolveCapabilityTarget({
4024
- activeSkill,
4025
- target: capabilityProvider.target,
4026
- targetRef: input.targetRef
4027
- }) : void 0;
4028
- if (capabilityProvider.target && !capabilityTarget?.value.trim()) {
4029
- throw new Error(
4030
- `jr-rpc issue-credential requires ${capabilityProvider.target.type} target context; use --target <value>`
4031
- );
3619
+ throw new Error("Credential enablement requires requester context");
4032
3620
  }
4033
- const declared = activeSkill?.requiresCapabilities ?? [];
4034
- if (activeSkill && !declared.includes(capability)) {
4035
- logWarn(
4036
- "capability_not_declared_for_skill",
4037
- {},
4038
- {
4039
- "app.skill.name": activeSkill.name,
4040
- "app.capability.name": capability
4041
- },
4042
- "Capability issued even though it is not declared in the active skill (soft enforcement)"
4043
- );
3621
+ const plugin = getPluginDefinition(provider);
3622
+ if (!plugin?.manifest.credentials) {
3623
+ return void 0;
4044
3624
  }
4045
- const cacheKey = this.capabilityCacheKey(capability, capabilityTarget);
4046
- const existing = this.enabledByCapability.get(cacheKey);
3625
+ const existing = this.enabledByProvider.get(provider);
4047
3626
  const now = Date.now();
4048
3627
  if (existing && existing.expiresAtMs - now > 1e4) {
4049
3628
  return {
@@ -4055,31 +3634,30 @@ var SkillCapabilityRuntime = class {
4055
3634
  "credential_issue_request",
4056
3635
  {},
4057
3636
  {
4058
- "app.skill.name": activeSkill?.name,
4059
- "app.capability.name": capability
3637
+ "app.skill.name": input.activeSkill?.name,
3638
+ "app.credential.provider": provider
4060
3639
  },
4061
- "Issuing capability credential for current turn"
3640
+ "Issuing provider credential for current turn"
4062
3641
  );
4063
3642
  try {
4064
- const lease = await this.issueCapabilityLease({
4065
- activeSkill,
4066
- capability,
4067
- targetRef: input.targetRef,
4068
- reason: input.reason
3643
+ const lease = await this.router.issue({
3644
+ provider,
3645
+ reason: input.reason,
3646
+ requesterId: this.requesterId
4069
3647
  });
4070
- const transforms = this.toHeaderTransforms(lease);
3648
+ const transforms = toHeaderTransforms(lease);
4071
3649
  if (transforms.length === 0) {
4072
3650
  throw new Error(
4073
- `Credential lease for ${capability} did not include header transforms`
3651
+ `Credential lease for ${provider} did not include header transforms`
4074
3652
  );
4075
3653
  }
4076
3654
  const expiresAtMs = Date.parse(lease.expiresAt);
4077
3655
  if (!Number.isFinite(expiresAtMs)) {
4078
3656
  throw new Error(
4079
- `Credential lease for ${capability} returned invalid expiresAt`
3657
+ `Credential lease for ${provider} returned invalid expiresAt`
4080
3658
  );
4081
3659
  }
4082
- this.enabledByCapability.set(cacheKey, {
3660
+ this.enabledByProvider.set(provider, {
4083
3661
  expiresAtMs,
4084
3662
  transforms,
4085
3663
  env: lease.env
@@ -4088,13 +3666,12 @@ var SkillCapabilityRuntime = class {
4088
3666
  "credential_issue_success",
4089
3667
  {},
4090
3668
  {
4091
- "app.skill.name": activeSkill?.name,
4092
- "app.capability.name": capability,
3669
+ "app.skill.name": input.activeSkill?.name,
4093
3670
  "app.credential.provider": lease.provider,
4094
3671
  "app.credential.expires_at": lease.expiresAt,
4095
3672
  "app.credential.delivery": "header_transform"
4096
3673
  },
4097
- "Issued capability credential lease"
3674
+ "Issued provider credential lease"
4098
3675
  );
4099
3676
  return { reused: false, expiresAt: lease.expiresAt };
4100
3677
  } catch (error) {
@@ -4102,11 +3679,11 @@ var SkillCapabilityRuntime = class {
4102
3679
  "credential_issue_failed",
4103
3680
  {},
4104
3681
  {
4105
- "app.skill.name": activeSkill?.name,
4106
- "app.capability.name": capability,
3682
+ "app.skill.name": input.activeSkill?.name,
3683
+ "app.credential.provider": provider,
4107
3684
  "error.message": error instanceof Error ? error.message : String(error)
4108
3685
  },
4109
- "Capability credential resolution failed"
3686
+ "Provider credential resolution failed"
4110
3687
  );
4111
3688
  throw error;
4112
3689
  }
@@ -4114,9 +3691,9 @@ var SkillCapabilityRuntime = class {
4114
3691
  getTurnHeaderTransforms() {
4115
3692
  const now = Date.now();
4116
3693
  const headerTransforms = [];
4117
- for (const [capability, entry] of this.enabledByCapability.entries()) {
3694
+ for (const [provider, entry] of this.enabledByProvider.entries()) {
4118
3695
  if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
4119
- this.enabledByCapability.delete(capability);
3696
+ this.enabledByProvider.delete(provider);
4120
3697
  continue;
4121
3698
  }
4122
3699
  headerTransforms.push(...entry.transforms);
@@ -4126,9 +3703,9 @@ var SkillCapabilityRuntime = class {
4126
3703
  getTurnEnv() {
4127
3704
  const now = Date.now();
4128
3705
  const env = {};
4129
- for (const [capability, entry] of this.enabledByCapability.entries()) {
3706
+ for (const [provider, entry] of this.enabledByProvider.entries()) {
4130
3707
  if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
4131
- this.enabledByCapability.delete(capability);
3708
+ this.enabledByProvider.delete(provider);
4132
3709
  continue;
4133
3710
  }
4134
3711
  Object.assign(env, entry.env);
@@ -4177,7 +3754,6 @@ var TestCredentialBroker = class {
4177
3754
  return {
4178
3755
  id: randomUUID2(),
4179
3756
  provider: this.config.provider,
4180
- capability: input.capability,
4181
3757
  env: {
4182
3758
  [this.config.envKey]: this.config.placeholder
4183
3759
  },
@@ -4190,8 +3766,7 @@ var TestCredentialBroker = class {
4190
3766
  })),
4191
3767
  expiresAt,
4192
3768
  metadata: {
4193
- reason: input.reason,
4194
- target: input.target ? `${input.target.type}:${input.target.value}` : "none"
3769
+ reason: input.reason
4195
3770
  }
4196
3771
  };
4197
3772
  }
@@ -4223,26 +3798,12 @@ function createSkillCapabilityRuntime(options = {}) {
4223
3798
  const router = new ProviderCredentialRouter({ brokersByProvider });
4224
3799
  return new SkillCapabilityRuntime({
4225
3800
  router,
4226
- invocationArgs: options.invocationArgs,
4227
- requesterId: options.requesterId,
4228
- resolveConfiguration: options.resolveConfiguration
3801
+ requesterId: options.requesterId
4229
3802
  });
4230
3803
  }
4231
3804
 
4232
3805
  // src/chat/capabilities/jr-rpc-command.ts
4233
3806
  import { Bash, defineCommand } from "just-bash";
4234
-
4235
- // src/chat/credentials/unlink-provider.ts
4236
- async function unlinkProvider(userId, provider, userTokenStore) {
4237
- await Promise.all([
4238
- userTokenStore.delete(userId, provider),
4239
- deleteMcpStoredOAuthCredentials(userId, provider),
4240
- deleteMcpServerSessionId(userId, provider),
4241
- deleteMcpAuthSessionsForUserProvider(userId, provider)
4242
- ]);
4243
- }
4244
-
4245
- // src/chat/capabilities/jr-rpc-command.ts
4246
3807
  function commandResult(input) {
4247
3808
  let stdout = "";
4248
3809
  if (typeof input.stdout === "string") {
@@ -4286,133 +3847,6 @@ function parsePrefixFlag(extras) {
4286
3847
  error: "jr-rpc config list accepts optional --prefix <value>\n"
4287
3848
  };
4288
3849
  }
4289
- async function handleIssueCredentialCommand(args, deps) {
4290
- const capability = (args[0] ?? "").trim();
4291
- if (!capability) {
4292
- return commandResult({
4293
- stderr: "jr-rpc issue-credential requires a capability argument\n",
4294
- exitCode: 2
4295
- });
4296
- }
4297
- let targetRef;
4298
- const extras = args.slice(1);
4299
- if (extras.length > 0) {
4300
- if (extras.length === 2 && extras[0] === "--target") {
4301
- targetRef = extras[1]?.trim();
4302
- } else if (extras.length === 1 && extras[0].startsWith("--target=")) {
4303
- targetRef = extras[0].slice("--target=".length).trim();
4304
- } else {
4305
- return {
4306
- stdout: "",
4307
- stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --target <value>\n",
4308
- exitCode: 2
4309
- };
4310
- }
4311
- if (!targetRef) {
4312
- return {
4313
- stdout: "",
4314
- stderr: "jr-rpc issue-credential --target requires a non-empty value\n",
4315
- exitCode: 2
4316
- };
4317
- }
4318
- }
4319
- let outcome;
4320
- try {
4321
- outcome = await deps.capabilityRuntime.enableCapabilityForTurn({
4322
- activeSkill: deps.activeSkill,
4323
- capability,
4324
- ...targetRef ? { targetRef } : {},
4325
- reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
4326
- });
4327
- } catch (error) {
4328
- if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
4329
- const authAction = deps.providerAuthActions?.get(error.provider);
4330
- if (authAction?.kind === "oauth_started") {
4331
- const providerLabel = formatProviderLabel(error.provider);
4332
- return commandResult({
4333
- stdout: {
4334
- credential_unavailable: true,
4335
- oauth_started: true,
4336
- provider: error.provider,
4337
- private_delivery_sent: authAction.delivered,
4338
- message: authAction.delivered ? `I've already sent you a private authorization link to connect your ${providerLabel} account. Finish that flow, then return to Slack.` : `I still need to connect your ${providerLabel} account, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
4339
- },
4340
- exitCode: 0
4341
- });
4342
- }
4343
- if (authAction?.kind === "token_deleted") {
4344
- const reconnectResult = await startOAuthFlow(error.provider, {
4345
- requesterId: deps.requesterId,
4346
- channelId: deps.channelId,
4347
- threadTs: deps.threadTs,
4348
- activeSkillName: deps.activeSkill?.name ?? void 0
4349
- // Intentionally no userMessage — reconnect flows must not auto-resume.
4350
- });
4351
- if (!reconnectResult.ok) {
4352
- return commandResult({
4353
- stderr: `${reconnectResult.error}
4354
- `,
4355
- exitCode: 1
4356
- });
4357
- }
4358
- const delivered = !!reconnectResult.delivery;
4359
- deps.providerAuthActions?.set(error.provider, {
4360
- kind: "oauth_started",
4361
- delivered
4362
- });
4363
- const providerLabel = formatProviderLabel(error.provider);
4364
- return commandResult({
4365
- stdout: {
4366
- credential_unavailable: true,
4367
- oauth_started: true,
4368
- provider: error.provider,
4369
- private_delivery_sent: delivered,
4370
- message: delivered ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
4371
- },
4372
- exitCode: 0
4373
- });
4374
- }
4375
- const oauthResult = await startOAuthFlow(error.provider, {
4376
- requesterId: deps.requesterId,
4377
- channelId: deps.channelId,
4378
- threadTs: deps.threadTs,
4379
- userMessage: deps.userMessage,
4380
- channelConfiguration: deps.channelConfiguration,
4381
- activeSkillName: deps.activeSkill?.name ?? void 0
4382
- });
4383
- if (oauthResult.ok) {
4384
- const providerLabel = formatProviderLabel(error.provider);
4385
- return commandResult({
4386
- stdout: {
4387
- credential_unavailable: true,
4388
- oauth_started: true,
4389
- provider: error.provider,
4390
- private_delivery_sent: !!oauthResult.delivery,
4391
- message: oauthResult.delivery ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
4392
- },
4393
- exitCode: 0
4394
- });
4395
- }
4396
- return {
4397
- stdout: "",
4398
- stderr: `${oauthResult.error}
4399
- `,
4400
- exitCode: 1
4401
- };
4402
- }
4403
- return {
4404
- stdout: "",
4405
- stderr: `${error instanceof Error ? error.message : String(error)}
4406
- `,
4407
- exitCode: 1
4408
- };
4409
- }
4410
- return commandResult({
4411
- stdout: `${outcome.reused ? "credential_reused" : "credential_enabled"} capability=${capability} expiresAt=${outcome.expiresAt}
4412
- `,
4413
- exitCode: 0
4414
- });
4415
- }
4416
3850
  async function handleConfigCommand(args, deps) {
4417
3851
  const usage = [
4418
3852
  "jr-rpc config get <key>",
@@ -4593,142 +4027,15 @@ ${usage}
4593
4027
  exitCode: 2
4594
4028
  });
4595
4029
  }
4596
- function isKnownProvider(provider) {
4597
- return listCapabilityProviders().some((p) => p.provider === provider) || isPluginProvider(provider);
4598
- }
4599
- async function handleOAuthStartCommand(args, deps) {
4600
- const provider = (args[0] ?? "").trim();
4601
- if (!provider) {
4602
- return commandResult({
4603
- stderr: "jr-rpc oauth-start requires: <provider>\n",
4604
- exitCode: 2
4605
- });
4606
- }
4607
- if (args.length > 1) {
4608
- return commandResult({
4609
- stderr: "jr-rpc oauth-start accepts only a provider argument\n",
4610
- exitCode: 2
4611
- });
4612
- }
4613
- if (deps.requesterId && deps.userTokenStore) {
4614
- const stored = await deps.userTokenStore.get(deps.requesterId, provider);
4615
- const providerConfig = getPluginOAuthConfig(provider);
4616
- if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) && hasRequiredOAuthScope(stored.scope, providerConfig?.scope)) {
4617
- const providerLabel = formatProviderLabel(provider);
4618
- return commandResult({
4619
- stdout: {
4620
- ok: true,
4621
- already_connected: true,
4622
- provider,
4623
- message: `Your ${providerLabel} account is already connected.`
4624
- },
4625
- exitCode: 0
4626
- });
4627
- }
4628
- }
4629
- if (!deps.requesterId) {
4630
- return commandResult({
4631
- stderr: "jr-rpc oauth-start requires requester context (requesterId)\n",
4632
- exitCode: 1
4633
- });
4634
- }
4635
- const result = await startOAuthFlow(provider, {
4636
- requesterId: deps.requesterId,
4637
- channelId: deps.channelId,
4638
- threadTs: deps.threadTs,
4639
- activeSkillName: deps.activeSkill?.name ?? void 0
4640
- });
4641
- if (!result.ok) {
4642
- return commandResult({ stderr: `${result.error}
4643
- `, exitCode: 1 });
4644
- }
4645
- deps.providerAuthActions?.set(provider, {
4646
- kind: "oauth_started",
4647
- delivered: !!result.delivery
4648
- });
4649
- if (!result.delivery) {
4650
- return commandResult({
4651
- stdout: {
4652
- ok: true,
4653
- private_delivery_sent: false,
4654
- message: "I wasn't able to send you a private authorization link. Please send me a direct message and try again."
4655
- },
4656
- exitCode: 0
4657
- });
4658
- }
4659
- return commandResult({
4660
- stdout: {
4661
- ok: true,
4662
- private_delivery_sent: true
4663
- },
4664
- exitCode: 0
4665
- });
4666
- }
4667
- async function handleDeleteTokenCommand(args, deps) {
4668
- const provider = (args[0] ?? "").trim();
4669
- if (!provider) {
4670
- return commandResult({
4671
- stderr: "jr-rpc delete-token requires: <provider>\n",
4672
- exitCode: 2
4673
- });
4674
- }
4675
- if (!isKnownProvider(provider)) {
4676
- return commandResult({
4677
- stderr: `Unknown provider: ${provider}
4678
- `,
4679
- exitCode: 2
4680
- });
4681
- }
4682
- if (!deps.requesterId) {
4683
- return commandResult({
4684
- stderr: "jr-rpc delete-token requires requester context (requesterId)\n",
4685
- exitCode: 1
4686
- });
4687
- }
4688
- if (!deps.userTokenStore) {
4689
- return commandResult({
4690
- stderr: "Token storage is not available\n",
4691
- exitCode: 1
4692
- });
4693
- }
4694
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
4695
- deps.providerAuthActions?.set(provider, { kind: "token_deleted" });
4696
- logInfo(
4697
- "jr_rpc_delete_token",
4698
- {},
4699
- {
4700
- "app.credential.provider": provider,
4701
- ...deps.activeSkill?.name ? { "app.skill.name": deps.activeSkill.name } : {}
4702
- },
4703
- "Deleted user token via jr-rpc"
4704
- );
4705
- return commandResult({
4706
- stdout: `token_deleted provider=${provider}
4707
- `,
4708
- exitCode: 0
4709
- });
4710
- }
4711
4030
  function createJrRpcCommand(deps) {
4712
4031
  return defineCommand("jr-rpc", async (args) => {
4713
4032
  const usage = [
4714
- "jr-rpc issue-credential <capability> [--target <value>]",
4715
- "jr-rpc oauth-start <provider>",
4716
- "jr-rpc delete-token <provider>",
4717
4033
  "jr-rpc config get <key>",
4718
4034
  "jr-rpc config set <key> <value> [--json]",
4719
4035
  "jr-rpc config unset <key>",
4720
4036
  "jr-rpc config list [--prefix <value>]"
4721
4037
  ].join("\n");
4722
4038
  const verb = (args[0] ?? "").trim();
4723
- if (verb === "issue-credential") {
4724
- return handleIssueCredentialCommand(args.slice(1), deps);
4725
- }
4726
- if (verb === "oauth-start") {
4727
- return handleOAuthStartCommand(args.slice(1), deps);
4728
- }
4729
- if (verb === "delete-token") {
4730
- return handleDeleteTokenCommand(args.slice(1), deps);
4731
- }
4732
4039
  if (verb === "config") {
4733
4040
  return handleConfigCommand(args.slice(1), deps);
4734
4041
  }
@@ -5810,7 +5117,6 @@ function toLoadedSkill(result, availableSkills) {
5810
5117
  skillPath: metadata?.skillPath ?? result.skill_dir,
5811
5118
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
5812
5119
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
5813
- ...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
5814
5120
  ...metadata?.usesConfig ? { usesConfig: metadata.usesConfig } : {},
5815
5121
  body: result.instructions
5816
5122
  };
@@ -5837,7 +5143,6 @@ async function loadSkillFromHost(availableSkills, skillName) {
5837
5143
  ok: true,
5838
5144
  skill_name: skill.name,
5839
5145
  description: skill.description,
5840
- ...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
5841
5146
  skill_dir: skillDir,
5842
5147
  location: skillFilePath,
5843
5148
  instructions: loaded.body
@@ -5845,7 +5150,7 @@ async function loadSkillFromHost(availableSkills, skillName) {
5845
5150
  }
5846
5151
  function createLoadSkillTool(availableSkills, options) {
5847
5152
  return tool({
5848
- description: "Load a skill by name so its instructions are available for this turn. The result includes `requires_capabilities` when the skill declares authenticated provider access, and `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
5153
+ description: "Load a skill by name so its instructions are available for this turn. The result includes `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
5849
5154
  inputSchema: Type4.Object({
5850
5155
  skill_name: Type4.String({
5851
5156
  minLength: 1,
@@ -8392,6 +7697,92 @@ fallbackToRealGh();
8392
7697
  `;
8393
7698
  }
8394
7699
 
7700
+ // src/chat/sandbox/eval-oauth-stub.ts
7701
+ function buildEvalOauthCliStub() {
7702
+ return `#!/usr/bin/env node
7703
+ const fs = require("node:fs");
7704
+
7705
+ const args = process.argv.slice(2);
7706
+
7707
+ function outputText(value) {
7708
+ fs.writeFileSync(process.stdout.fd, value);
7709
+ }
7710
+
7711
+ if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
7712
+ outputText("eval-oauth 1.0.0 (junior-eval)\\n");
7713
+ process.exit(0);
7714
+ }
7715
+
7716
+ if (args[0] === "whoami") {
7717
+ outputText("eval-oauth-user\\n");
7718
+ process.exit(0);
7719
+ }
7720
+
7721
+ process.stderr.write("eval-oauth stub: unsupported command\\n");
7722
+ process.exit(1);
7723
+ `;
7724
+ }
7725
+
7726
+ // src/chat/sandbox/eval-sentry-stub.ts
7727
+ function buildEvalSentryCliStub() {
7728
+ return `#!/usr/bin/env node
7729
+ const fs = require("node:fs");
7730
+ const { spawnSync } = require("node:child_process");
7731
+
7732
+ const args = process.argv.slice(2);
7733
+ const fallbackBinaries = ["/usr/bin/sentry", "/usr/local/bin/sentry", "/bin/sentry"];
7734
+
7735
+ function hasFlag(name) {
7736
+ return args.includes(name) || args.some((value) => value.startsWith(name + "="));
7737
+ }
7738
+
7739
+ function outputJson(value) {
7740
+ fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
7741
+ }
7742
+
7743
+ function outputText(value) {
7744
+ fs.writeFileSync(process.stdout.fd, value);
7745
+ }
7746
+
7747
+ function fallbackToRealSentry() {
7748
+ for (const binary of fallbackBinaries) {
7749
+ if (!fs.existsSync(binary)) {
7750
+ continue;
7751
+ }
7752
+ const result = spawnSync(binary, args, { stdio: "inherit" });
7753
+ process.exit(result.status ?? 1);
7754
+ }
7755
+ process.stderr.write("sentry stub: unsupported command\\n");
7756
+ process.exit(1);
7757
+ }
7758
+
7759
+ if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
7760
+ outputText("sentry-cli 2.0.0 (junior-eval)\\n");
7761
+ process.exit(0);
7762
+ }
7763
+
7764
+ if (args[0] === "issues" && args[1] === "list") {
7765
+ if (hasFlag("--json")) {
7766
+ outputJson([]);
7767
+ } else {
7768
+ outputText("No issues found.\\n");
7769
+ }
7770
+ process.exit(0);
7771
+ }
7772
+
7773
+ if (args[0] === "organizations" && args[1] === "list") {
7774
+ if (hasFlag("--json")) {
7775
+ outputJson([{ slug: "getsentry", name: "Sentry" }]);
7776
+ } else {
7777
+ outputText("getsentry\\n");
7778
+ }
7779
+ process.exit(0);
7780
+ }
7781
+
7782
+ fallbackToRealSentry();
7783
+ `;
7784
+ }
7785
+
8395
7786
  // src/chat/sandbox/skill-sync.ts
8396
7787
  function toPosixRelative(base, absolute) {
8397
7788
  return path5.relative(base, absolute).split(path5.sep).join("/");
@@ -8455,6 +7846,16 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
8455
7846
  path: `${runtimeBinDir}/gh`,
8456
7847
  content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
8457
7848
  });
7849
+ filesToWrite.push({
7850
+ path: `${runtimeBinDir}/sentry`,
7851
+ content: Buffer.from(buildEvalSentryCliStub(), "utf8")
7852
+ });
7853
+ }
7854
+ if (availableSkills.some((skill) => skill.name === "eval-oauth")) {
7855
+ filesToWrite.push({
7856
+ path: `${runtimeBinDir}/eval-oauth`,
7857
+ content: Buffer.from(buildEvalOauthCliStub(), "utf8")
7858
+ });
8458
7859
  }
8459
7860
  return filesToWrite;
8460
7861
  }
@@ -9411,6 +8812,135 @@ function shouldEmitDevAgentTrace() {
9411
8812
  return process.env.NODE_ENV === "development";
9412
8813
  }
9413
8814
 
8815
+ // src/chat/credentials/unlink-provider.ts
8816
+ async function unlinkProvider(userId, provider, userTokenStore) {
8817
+ await Promise.all([
8818
+ userTokenStore.delete(userId, provider),
8819
+ deleteMcpStoredOAuthCredentials(userId, provider),
8820
+ deleteMcpServerSessionId(userId, provider),
8821
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
8822
+ ]);
8823
+ }
8824
+
8825
+ // src/chat/services/plugin-auth-orchestration.ts
8826
+ var PluginAuthorizationPauseError = class extends Error {
8827
+ provider;
8828
+ constructor(provider) {
8829
+ super(`Plugin authorization started for ${provider}`);
8830
+ this.name = "PluginAuthorizationPauseError";
8831
+ this.provider = provider;
8832
+ }
8833
+ };
8834
+ function isCommandAuthFailure(details) {
8835
+ if (!details || typeof details !== "object") {
8836
+ return false;
8837
+ }
8838
+ const result = details;
8839
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
8840
+ return false;
8841
+ }
8842
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
8843
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
8844
+ if (!text.trim()) {
8845
+ return false;
8846
+ }
8847
+ return [
8848
+ /\b401\b/,
8849
+ /\bunauthorized\b/,
8850
+ /\bbad credentials\b/,
8851
+ /\binvalid token\b/,
8852
+ /\btoken (?:expired|revoked)\b/,
8853
+ /\bexpired token\b/,
8854
+ /\bmissing scopes?\b/,
8855
+ /\binsufficient scope\b/,
8856
+ /\binvalid grant\b/,
8857
+ /\breauthoriz/
8858
+ ].some((pattern) => pattern.test(text));
8859
+ }
8860
+ function commandTargetsProvider(provider, command, details) {
8861
+ const normalizedCommand = command.trim().toLowerCase();
8862
+ if (!normalizedCommand) {
8863
+ return false;
8864
+ }
8865
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
8866
+ return true;
8867
+ }
8868
+ const plugin = getPluginDefinition(provider);
8869
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
8870
+ const credentials = plugin?.manifest.credentials;
8871
+ if (credentials) {
8872
+ candidates.add(credentials.authTokenEnv.toLowerCase());
8873
+ for (const domain of credentials.apiDomains) {
8874
+ candidates.add(domain.toLowerCase());
8875
+ }
8876
+ }
8877
+ const combinedText = `${normalizedCommand}
8878
+ ${details.stdout?.toLowerCase() ?? ""}
8879
+ ${details.stderr?.toLowerCase() ?? ""}`;
8880
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
8881
+ }
8882
+ function createPluginAuthOrchestration(deps, abortAgent) {
8883
+ let pendingPause;
8884
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
8885
+ if (pendingPause) {
8886
+ throw pendingPause;
8887
+ }
8888
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
8889
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
8890
+ }
8891
+ const providerLabel = formatProviderLabel(provider);
8892
+ const oauthResult = await startOAuthFlow(provider, {
8893
+ requesterId: deps.requesterId,
8894
+ channelId: deps.channelId,
8895
+ threadTs: deps.threadTs,
8896
+ userMessage: deps.userMessage,
8897
+ channelConfiguration: deps.channelConfiguration,
8898
+ activeSkillName: activeSkill?.name ?? void 0,
8899
+ resumeConversationId: deps.conversationId,
8900
+ resumeSessionId: deps.sessionId
8901
+ });
8902
+ if (!oauthResult.ok) {
8903
+ throw new Error(oauthResult.error);
8904
+ }
8905
+ if (!oauthResult.delivery) {
8906
+ throw new Error(
8907
+ `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
8908
+ );
8909
+ }
8910
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
8911
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
8912
+ }
8913
+ pendingPause = new PluginAuthorizationPauseError(provider);
8914
+ abortAgent();
8915
+ throw pendingPause;
8916
+ };
8917
+ const handleCredentialUnavailable = async (input) => {
8918
+ if (pendingPause) {
8919
+ throw pendingPause;
8920
+ }
8921
+ if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
8922
+ throw input.error;
8923
+ }
8924
+ return await startAuthorizationPause(
8925
+ input.error.provider,
8926
+ input.activeSkill
8927
+ );
8928
+ };
8929
+ return {
8930
+ handleCredentialUnavailable,
8931
+ handleCommandFailure: async (input) => {
8932
+ const provider = input.activeSkill?.pluginProvider;
8933
+ if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
8934
+ return;
8935
+ }
8936
+ await startAuthorizationPause(provider, input.activeSkill, {
8937
+ unlinkExistingProvider: true
8938
+ });
8939
+ },
8940
+ getPendingPause: () => pendingPause
8941
+ };
8942
+ }
8943
+
9414
8944
  // src/chat/runtime/tool-status.ts
9415
8945
  function buildToolStatus(toolName, input) {
9416
8946
  const obj = input && typeof input === "object" ? input : void 0;
@@ -9612,7 +9142,7 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
9612
9142
  }
9613
9143
 
9614
9144
  // src/chat/tools/agent-tools.ts
9615
- function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, capabilityRuntime, hooks) {
9145
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, capabilityRuntime, pluginAuthOrchestration, hooks) {
9616
9146
  const shouldTrace = shouldEmitDevAgentTrace();
9617
9147
  return Object.entries(tools).map(([toolName, toolDef]) => ({
9618
9148
  name: toolName,
@@ -9670,6 +9200,13 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9670
9200
  experimental_context: sandbox
9671
9201
  });
9672
9202
  const normalized = normalizeToolResult(result, isSandbox);
9203
+ if (bashCommand && pluginAuthOrchestration) {
9204
+ await pluginAuthOrchestration.handleCommandFailure({
9205
+ activeSkill: sandbox.getActiveSkill(),
9206
+ command: bashCommand,
9207
+ details: normalized.details
9208
+ });
9209
+ }
9673
9210
  const toolResultAttribute = serializeGenAiAttribute(
9674
9211
  normalized.details
9675
9212
  );
@@ -9680,6 +9217,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9680
9217
  }
9681
9218
  return normalized;
9682
9219
  } catch (error) {
9220
+ if (error instanceof PluginAuthorizationPauseError) {
9221
+ throw error;
9222
+ }
9683
9223
  handleToolExecutionError(
9684
9224
  error,
9685
9225
  toolName,
@@ -9699,7 +9239,234 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9699
9239
  }
9700
9240
  );
9701
9241
  }
9702
- }));
9242
+ }));
9243
+ }
9244
+
9245
+ // src/chat/respond-helpers.ts
9246
+ var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
9247
+ function getSessionIdentifiers(context) {
9248
+ return {
9249
+ conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
9250
+ sessionId: context.correlation?.turnId
9251
+ };
9252
+ }
9253
+ function isExecutionDeferralResponse(text) {
9254
+ return /\b(want me to proceed|do you want me to proceed|shall i proceed|can i proceed|should i proceed|let me do that now|give me a moment|tag me again|fresh invocation)\b/i.test(
9255
+ text
9256
+ );
9257
+ }
9258
+ function isToolAccessDisclaimerResponse(text) {
9259
+ return /\b(i (don't|do not) have access to (active )?tool|tool results came back empty|prior results .* empty|cannot access .*tool|need to (run|load) .*tool .* first)\b/i.test(
9260
+ text
9261
+ );
9262
+ }
9263
+ function isExecutionEscapeResponse(text) {
9264
+ const trimmed = text.trim();
9265
+ if (!trimmed) return false;
9266
+ return isExecutionDeferralResponse(trimmed) || isToolAccessDisclaimerResponse(trimmed);
9267
+ }
9268
+ function parseJsonCandidate2(text) {
9269
+ const trimmed = text.trim();
9270
+ if (!trimmed) return void 0;
9271
+ try {
9272
+ return JSON.parse(trimmed);
9273
+ } catch {
9274
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
9275
+ if (!fenced) return void 0;
9276
+ try {
9277
+ return JSON.parse(fenced[1]);
9278
+ } catch {
9279
+ return void 0;
9280
+ }
9281
+ }
9282
+ }
9283
+ function isToolPayloadShape(payload) {
9284
+ if (!payload || typeof payload !== "object") return false;
9285
+ const record = payload;
9286
+ const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
9287
+ if (type.startsWith("tool-")) return true;
9288
+ if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
9289
+ return true;
9290
+ const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
9291
+ const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
9292
+ if (hasToolName && hasToolInput) return true;
9293
+ return false;
9294
+ }
9295
+ function isRawToolPayloadResponse(text) {
9296
+ const parsed = parseJsonCandidate2(text);
9297
+ if (Array.isArray(parsed)) {
9298
+ return parsed.some((entry) => isToolPayloadShape(entry));
9299
+ }
9300
+ if (isToolPayloadShape(parsed)) {
9301
+ return true;
9302
+ }
9303
+ const compact = text.replace(/\s+/g, " ");
9304
+ return /"type"\s*:\s*"tool[-_](use|call|result|error)"/i.test(compact);
9305
+ }
9306
+ function toObservablePromptPart(part) {
9307
+ if (part.type === "text") {
9308
+ return {
9309
+ type: "text",
9310
+ text: part.text
9311
+ };
9312
+ }
9313
+ return {
9314
+ type: "image",
9315
+ mimeType: part.mimeType,
9316
+ data: `[omitted:${part.data.length}]`
9317
+ };
9318
+ }
9319
+ function summarizeMessageText(text) {
9320
+ const normalized = text.trim().replace(/\s+/g, " ");
9321
+ if (!normalized) {
9322
+ return "[empty]";
9323
+ }
9324
+ return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
9325
+ }
9326
+ function buildUserTurnText(userInput, conversationContext, metadata) {
9327
+ const trimmedContext = conversationContext?.trim();
9328
+ const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
9329
+ const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
9330
+ if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
9331
+ return userInput;
9332
+ }
9333
+ const sections = [
9334
+ "<current-message>",
9335
+ userInput,
9336
+ "</current-message>"
9337
+ ];
9338
+ if (trimmedContext) {
9339
+ sections.push(
9340
+ "",
9341
+ "<thread-conversation-context>",
9342
+ "Use this context for continuity across prior thread turns.",
9343
+ trimmedContext,
9344
+ "</thread-conversation-context>"
9345
+ );
9346
+ }
9347
+ if (metadata?.sessionContext?.conversationId) {
9348
+ sections.push(
9349
+ "",
9350
+ "<session-context>",
9351
+ `- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
9352
+ "</session-context>"
9353
+ );
9354
+ }
9355
+ if (metadata?.turnContext?.traceId) {
9356
+ sections.push(
9357
+ "",
9358
+ "<turn-context>",
9359
+ `- trace_id: ${metadata.turnContext.traceId}`,
9360
+ "</turn-context>"
9361
+ );
9362
+ }
9363
+ return sections.join("\n");
9364
+ }
9365
+ function encodeNonImageAttachmentForPrompt(attachment) {
9366
+ const base64 = attachment.data.toString("base64");
9367
+ const wasTruncated = base64.length > MAX_INLINE_ATTACHMENT_BASE64_CHARS;
9368
+ const encodedPayload = wasTruncated ? `${base64.slice(0, MAX_INLINE_ATTACHMENT_BASE64_CHARS)}...` : base64;
9369
+ return [
9370
+ "<attachment>",
9371
+ `filename: ${attachment.filename ?? "unnamed"}`,
9372
+ `media_type: ${attachment.mediaType}`,
9373
+ "encoding: base64",
9374
+ `truncated: ${wasTruncated ? "true" : "false"}`,
9375
+ "<data_base64>",
9376
+ encodedPayload,
9377
+ "</data_base64>",
9378
+ "</attachment>"
9379
+ ].join("\n");
9380
+ }
9381
+ function buildExecutionFailureMessage(toolErrorCount) {
9382
+ if (toolErrorCount > 0) {
9383
+ return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
9384
+ }
9385
+ return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
9386
+ }
9387
+ function isToolResultMessage(value) {
9388
+ return typeof value === "object" && value !== null && value.role === "toolResult";
9389
+ }
9390
+ function normalizeToolNameFromResult(result) {
9391
+ if (!result || typeof result !== "object") return void 0;
9392
+ const record = result;
9393
+ if (typeof record.toolName === "string" && record.toolName.length > 0) {
9394
+ return record.toolName;
9395
+ }
9396
+ if (typeof record.name === "string" && record.name.length > 0) {
9397
+ return record.name;
9398
+ }
9399
+ return void 0;
9400
+ }
9401
+ function isToolResultError(result) {
9402
+ if (!result || typeof result !== "object") return false;
9403
+ return Boolean(result.isError);
9404
+ }
9405
+ function isAssistantMessage(value) {
9406
+ return typeof value === "object" && value !== null && value.role === "assistant";
9407
+ }
9408
+ function getPiMessageRole(value) {
9409
+ if (!value || typeof value !== "object") {
9410
+ return void 0;
9411
+ }
9412
+ const role = value.role;
9413
+ return typeof role === "string" ? role : void 0;
9414
+ }
9415
+ function extractAssistantText(message) {
9416
+ const content = message.content ?? [];
9417
+ return content.filter(
9418
+ (part) => part.type === "text" && typeof part.text === "string"
9419
+ ).map((part) => part.text).join("\n");
9420
+ }
9421
+ function getTerminalAssistantMessages(messages) {
9422
+ let lastToolResultIndex = -1;
9423
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
9424
+ if (isToolResultMessage(messages[index])) {
9425
+ lastToolResultIndex = index;
9426
+ break;
9427
+ }
9428
+ }
9429
+ return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
9430
+ }
9431
+ function hasCompletedAssistantTurn(messages) {
9432
+ const message = getTerminalAssistantMessages(messages).at(-1);
9433
+ if (!message) {
9434
+ return false;
9435
+ }
9436
+ const stopReason = message.stopReason;
9437
+ return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
9438
+ }
9439
+ function upsertActiveSkill(activeSkills, next) {
9440
+ const existing = activeSkills.find((skill) => skill.name === next.name);
9441
+ if (existing) {
9442
+ existing.body = next.body;
9443
+ existing.description = next.description;
9444
+ existing.skillPath = next.skillPath;
9445
+ existing.allowedTools = next.allowedTools;
9446
+ existing.usesConfig = next.usesConfig;
9447
+ existing.pluginProvider = next.pluginProvider;
9448
+ return;
9449
+ }
9450
+ activeSkills.push(next);
9451
+ }
9452
+ function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
9453
+ const keys = /* @__PURE__ */ new Set();
9454
+ for (const skill of [
9455
+ ...activeSkills,
9456
+ ...explicitSkill ? [explicitSkill] : []
9457
+ ]) {
9458
+ for (const key of skill.usesConfig ?? []) {
9459
+ keys.add(key);
9460
+ }
9461
+ }
9462
+ return [...keys].sort((a, b) => a.localeCompare(b));
9463
+ }
9464
+ function trimTrailingAssistantMessages(messages) {
9465
+ let end = messages.length;
9466
+ while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
9467
+ end -= 1;
9468
+ }
9469
+ return end === messages.length ? [...messages] : messages.slice(0, end);
9703
9470
  }
9704
9471
 
9705
9472
  // src/chat/services/reply-delivery-plan.ts
@@ -9801,7 +9568,6 @@ function buildTurnResult(input) {
9801
9568
  const assistantMessages = newMessages.filter(isAssistantMessage);
9802
9569
  const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
9803
9570
  const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9804
- const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
9805
9571
  const toolErrorCount = toolResults.filter((result) => result.isError).length;
9806
9572
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
9807
9573
  const successfulToolNames = new Set(
@@ -9810,13 +9576,14 @@ function buildTurnResult(input) {
9810
9576
  const channelPostPerformed = successfulToolNames.has(
9811
9577
  "slackChannelPostMessage"
9812
9578
  );
9813
- const deliveryPlan = buildReplyDeliveryPlan({
9579
+ const reactionPerformed = successfulToolNames.has("slackMessageAddReaction");
9580
+ const baseDeliveryPlan = buildReplyDeliveryPlan({
9814
9581
  explicitChannelPostIntent,
9815
9582
  channelPostPerformed,
9816
9583
  hasFiles: replyFiles.length > 0
9817
9584
  });
9818
- const deliveryMode = deliveryPlan.mode;
9819
- if (!primaryText && !oauthStartedMessage) {
9585
+ const sideEffectOnlySuccess = !primaryText && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
9586
+ if (!primaryText && !sideEffectOnlySuccess) {
9820
9587
  logWarn(
9821
9588
  "ai_model_response_empty",
9822
9589
  {
@@ -9839,12 +9606,17 @@ function buildTurnResult(input) {
9839
9606
  const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
9840
9607
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
9841
9608
  const usedPrimaryText = Boolean(primaryText);
9842
- const outcome = primaryText || oauthStartedMessage ? stopReason === "error" ? "provider_error" : "success" : "execution_failure";
9843
- const fallbackText = oauthStartedMessage ?? buildExecutionFailureMessage(toolErrorCount);
9844
- const responseText = primaryText || fallbackText;
9609
+ const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : sideEffectOnlySuccess ? "success" : "execution_failure";
9610
+ const fallbackText = buildExecutionFailureMessage(toolErrorCount);
9611
+ const responseText = primaryText || (sideEffectOnlySuccess ? "" : fallbackText);
9845
9612
  const escapedOrRawPayload = Boolean(primaryText) && (isExecutionEscapeResponse(primaryText) || isRawToolPayloadResponse(primaryText));
9846
9613
  const resolvedText = escapedOrRawPayload ? fallbackText : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
9847
- const resolvedOutcome = escapedOrRawPayload ? oauthStartedMessage ? outcome : "execution_failure" : outcome;
9614
+ const deliveryPlan = reactionPerformed && !resolvedText && replyFiles.length === 0 && !channelPostPerformed ? {
9615
+ ...baseDeliveryPlan,
9616
+ postThreadText: false
9617
+ } : baseDeliveryPlan;
9618
+ const deliveryMode = deliveryPlan.mode;
9619
+ const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
9848
9620
  if (shouldTrace) {
9849
9621
  logInfo(
9850
9622
  "agent_message_out",
@@ -10283,11 +10055,9 @@ async function generateAssistantReply(messageText, context = {}) {
10283
10055
  ...persistedConfigurationValues
10284
10056
  };
10285
10057
  const capabilityRuntime = createSkillCapabilityRuntime({
10286
- invocationArgs: skillInvocation?.args,
10287
- requesterId: context.requester?.userId,
10288
- resolveConfiguration: async (key) => configurationValues[key]
10058
+ requesterId: context.requester?.userId
10289
10059
  });
10290
- const providerAuthActions = /* @__PURE__ */ new Map();
10060
+ const userTokenStore = createUserTokenStore();
10291
10061
  sandboxExecutor = createSandboxExecutor({
10292
10062
  sandboxId: context.sandbox?.sandboxId,
10293
10063
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
@@ -10300,15 +10070,9 @@ async function generateAssistantReply(messageText, context = {}) {
10300
10070
  },
10301
10071
  runBashCustomCommand: async (command) => {
10302
10072
  const result = await maybeExecuteJrRpcCustomCommand(command, {
10303
- capabilityRuntime,
10304
10073
  activeSkill: skillSandbox.getActiveSkill(),
10305
10074
  channelConfiguration: context.channelConfiguration,
10306
10075
  requesterId: context.requester?.userId,
10307
- channelId: context.correlation?.channelId,
10308
- threadTs: context.correlation?.threadTs,
10309
- userMessage: userInput,
10310
- userTokenStore: createUserTokenStore(),
10311
- providerAuthActions,
10312
10076
  onConfigurationValueChanged: (key, value) => {
10313
10077
  if (value === void 0) {
10314
10078
  delete configurationValues[key];
@@ -10408,14 +10172,47 @@ async function generateAssistantReply(messageText, context = {}) {
10408
10172
  },
10409
10173
  () => agent?.abort()
10410
10174
  );
10175
+ const pluginAuth = createPluginAuthOrchestration(
10176
+ {
10177
+ conversationId: sessionConversationId,
10178
+ sessionId,
10179
+ requesterId: context.requester?.userId,
10180
+ channelId: context.correlation?.channelId,
10181
+ threadTs: context.correlation?.threadTs,
10182
+ userMessage: userInput,
10183
+ channelConfiguration: context.channelConfiguration,
10184
+ userTokenStore
10185
+ },
10186
+ () => agent?.abort()
10187
+ );
10411
10188
  mcpToolManager = new McpToolManager(getPluginMcpProviders(), {
10412
10189
  authProviderFactory: mcpAuth.authProviderFactory,
10413
10190
  onAuthorizationRequired: mcpAuth.onAuthorizationRequired
10414
10191
  });
10415
10192
  const turnMcpToolManager = mcpToolManager;
10193
+ const getPendingAuthPause = () => pluginAuth.getPendingPause() ?? mcpAuth.getPendingPause();
10416
10194
  const syncResumeState = () => {
10417
10195
  loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
10418
10196
  };
10197
+ const enableSkillCredentials = async (skill, reason) => {
10198
+ if (!skill?.pluginProvider) {
10199
+ return;
10200
+ }
10201
+ try {
10202
+ await capabilityRuntime.enableCredentialsForTurn({
10203
+ activeSkill: skill,
10204
+ reason
10205
+ });
10206
+ } catch (error) {
10207
+ if (error instanceof CredentialUnavailableError && context.requester?.userId) {
10208
+ await pluginAuth.handleCredentialUnavailable({
10209
+ activeSkill: skill,
10210
+ error
10211
+ });
10212
+ }
10213
+ throw error;
10214
+ }
10215
+ };
10419
10216
  setTags({
10420
10217
  conversationId: spanContext.conversationId,
10421
10218
  turnId: spanContext.turnId,
@@ -10457,6 +10254,10 @@ async function generateAssistantReply(messageText, context = {}) {
10457
10254
  if (mcpAuth.getPendingPause()) {
10458
10255
  return void 0;
10459
10256
  }
10257
+ await enableSkillCredentials(
10258
+ effective,
10259
+ `skill:${effective.name}:turn:load`
10260
+ );
10460
10261
  if (!effective.pluginProvider) {
10461
10262
  return void 0;
10462
10263
  }
@@ -10491,6 +10292,7 @@ async function generateAssistantReply(messageText, context = {}) {
10491
10292
  timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
10492
10293
  throw mcpAuth.getPendingPause();
10493
10294
  }
10295
+ await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
10494
10296
  }
10495
10297
  syncResumeState();
10496
10298
  const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
@@ -10581,6 +10383,7 @@ async function generateAssistantReply(messageText, context = {}) {
10581
10383
  context.onStatus,
10582
10384
  sandboxExecutor,
10583
10385
  capabilityRuntime,
10386
+ pluginAuth,
10584
10387
  agentToolHooks
10585
10388
  );
10586
10389
  const agentTools = [...baseAgentTools];
@@ -10594,6 +10397,7 @@ async function generateAssistantReply(messageText, context = {}) {
10594
10397
  context.onStatus,
10595
10398
  sandboxExecutor,
10596
10399
  capabilityRuntime,
10400
+ pluginAuth,
10597
10401
  agentToolHooks
10598
10402
  );
10599
10403
  agentTools.length = 0;
@@ -10700,9 +10504,9 @@ async function generateAssistantReply(messageText, context = {}) {
10700
10504
  });
10701
10505
  timeoutResumeMessages = [...agent.state.messages];
10702
10506
  }
10703
- if (mcpAuth.getPendingPause()) {
10507
+ if (getPendingAuthPause()) {
10704
10508
  timeoutResumeMessages = [...agent.state.messages];
10705
- throw mcpAuth.getPendingPause();
10509
+ throw getPendingAuthPause();
10706
10510
  }
10707
10511
  throw error;
10708
10512
  } finally {
@@ -10712,9 +10516,9 @@ async function generateAssistantReply(messageText, context = {}) {
10712
10516
  }
10713
10517
  newMessages = agent.state.messages.slice(beforeMessageCount);
10714
10518
  completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
10715
- if (mcpAuth.getPendingPause() && !completedAssistantTurn) {
10519
+ if (getPendingAuthPause() && !completedAssistantTurn) {
10716
10520
  timeoutResumeMessages = [...agent.state.messages];
10717
- throw mcpAuth.getPendingPause();
10521
+ throw getPendingAuthPause();
10718
10522
  }
10719
10523
  const outputMessages = newMessages.filter(isAssistantMessage);
10720
10524
  const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
@@ -10723,7 +10527,9 @@ async function generateAssistantReply(messageText, context = {}) {
10723
10527
  agent.state,
10724
10528
  ...outputMessages
10725
10529
  );
10726
- turnUsage = usageSummary.inputTokens !== void 0 || usageSummary.outputTokens !== void 0 || usageSummary.totalTokens !== void 0 ? usageSummary : void 0;
10530
+ turnUsage = Object.values(usageSummary).some(
10531
+ (value) => value !== void 0
10532
+ ) ? usageSummary : void 0;
10727
10533
  setSpanAttributes({
10728
10534
  ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
10729
10535
  ...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
@@ -10740,8 +10546,8 @@ async function generateAssistantReply(messageText, context = {}) {
10740
10546
  } finally {
10741
10547
  unsubscribe();
10742
10548
  }
10743
- if (mcpAuth.getPendingPause() && !completedAssistantTurn) {
10744
- throw mcpAuth.getPendingPause();
10549
+ if (getPendingAuthPause() && !completedAssistantTurn) {
10550
+ throw getPendingAuthPause();
10745
10551
  }
10746
10552
  if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
10747
10553
  await persistCompletedCheckpoint({
@@ -10799,7 +10605,7 @@ async function generateAssistantReply(messageText, context = {}) {
10799
10605
  );
10800
10606
  }
10801
10607
  }
10802
- if (error instanceof McpAuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
10608
+ if ((error instanceof McpAuthorizationPauseError || error instanceof PluginAuthorizationPauseError) && timeoutResumeConversationId && timeoutResumeSessionId) {
10803
10609
  const nextSliceId = await persistAuthPauseCheckpoint({
10804
10610
  conversationId: timeoutResumeConversationId,
10805
10611
  sessionId: timeoutResumeSessionId,
@@ -10817,7 +10623,7 @@ async function generateAssistantReply(messageText, context = {}) {
10817
10623
  }
10818
10624
  });
10819
10625
  throw new RetryableTurnError(
10820
- "mcp_auth_resume",
10626
+ error instanceof PluginAuthorizationPauseError ? "plugin_auth_resume" : "mcp_auth_resume",
10821
10627
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
10822
10628
  {
10823
10629
  conversationId: timeoutResumeConversationId,
@@ -10894,13 +10700,19 @@ function formatSlackDuration(durationMs) {
10894
10700
  return `${Math.round(durationSeconds)}s`;
10895
10701
  }
10896
10702
  function resolveTotalTokens(usage) {
10897
- if (usage?.totalTokens !== void 0) {
10898
- return usage.totalTokens;
10703
+ if (!usage) {
10704
+ return void 0;
10899
10705
  }
10900
- if (usage?.inputTokens !== void 0 && usage.outputTokens !== void 0) {
10901
- return usage.inputTokens + usage.outputTokens;
10706
+ const components = [
10707
+ usage.inputTokens,
10708
+ usage.outputTokens,
10709
+ usage.cachedInputTokens,
10710
+ usage.cacheCreationTokens
10711
+ ].filter((value) => value !== void 0);
10712
+ if (components.length > 0) {
10713
+ return components.reduce((sum, value) => sum + value, 0);
10902
10714
  }
10903
- return void 0;
10715
+ return usage.totalTokens;
10904
10716
  }
10905
10717
  function buildSlackReplyFooter(args) {
10906
10718
  const items = [];
@@ -11265,7 +11077,7 @@ async function resumeSlackTurn(args) {
11265
11077
  await args.onSuccess?.(reply);
11266
11078
  } catch (error) {
11267
11079
  await status.stop();
11268
- if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
11080
+ if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && args.onAuthPause) {
11269
11081
  deferredPauseHandler = async () => {
11270
11082
  await args.onAuthPause?.(error);
11271
11083
  };
@@ -11880,6 +11692,186 @@ async function buildResumeConversationContext2(channelId, threadTs) {
11880
11692
  excludeMessageId: latestUserMessageId
11881
11693
  });
11882
11694
  }
11695
+ async function buildCheckpointConversationContext(conversationId, sessionId) {
11696
+ const conversation = coerceThreadConversationState(
11697
+ await getPersistedThreadState(conversationId)
11698
+ );
11699
+ const userMessage = getTurnUserMessage(conversation, sessionId);
11700
+ return buildConversationContext(conversation, {
11701
+ excludeMessageId: userMessage?.id
11702
+ });
11703
+ }
11704
+ async function persistCompletedOAuthReplyState(args) {
11705
+ const currentState = await getPersistedThreadState(args.conversationId);
11706
+ const conversation = coerceThreadConversationState(currentState);
11707
+ const artifacts = coerceThreadArtifactsState(currentState);
11708
+ const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
11709
+ const userMessage = getTurnUserMessage(conversation, args.sessionId);
11710
+ markConversationMessage(conversation, userMessage?.id, {
11711
+ replied: true,
11712
+ skippedReason: void 0
11713
+ });
11714
+ upsertConversationMessage(conversation, {
11715
+ id: generateConversationId("assistant"),
11716
+ role: "assistant",
11717
+ text: normalizeConversationText(args.reply.text) || "[empty response]",
11718
+ createdAtMs: Date.now(),
11719
+ author: {
11720
+ userName: botConfig.userName,
11721
+ isBot: true
11722
+ },
11723
+ meta: {
11724
+ replied: true
11725
+ }
11726
+ });
11727
+ markTurnCompleted({
11728
+ conversation,
11729
+ nowMs: Date.now(),
11730
+ updateConversationStats
11731
+ });
11732
+ await persistThreadStateById(args.conversationId, {
11733
+ artifacts: nextArtifacts,
11734
+ conversation,
11735
+ sandboxId: args.reply.sandboxId,
11736
+ sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
11737
+ });
11738
+ }
11739
+ async function persistFailedOAuthReplyState(args) {
11740
+ const currentState = await getPersistedThreadState(args.conversationId);
11741
+ const conversation = coerceThreadConversationState(currentState);
11742
+ markTurnFailed({
11743
+ conversation,
11744
+ nowMs: Date.now(),
11745
+ userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
11746
+ markConversationMessage,
11747
+ updateConversationStats
11748
+ });
11749
+ await persistThreadStateById(args.conversationId, {
11750
+ conversation
11751
+ });
11752
+ }
11753
+ async function resumeCheckpointedOAuthTurn(stored) {
11754
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
11755
+ return false;
11756
+ }
11757
+ const checkpoint = await getAgentTurnSessionCheckpoint(
11758
+ stored.resumeConversationId,
11759
+ stored.resumeSessionId
11760
+ );
11761
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
11762
+ return false;
11763
+ }
11764
+ const currentState = await getPersistedThreadState(
11765
+ stored.resumeConversationId
11766
+ );
11767
+ const conversation = coerceThreadConversationState(currentState);
11768
+ const artifacts = coerceThreadArtifactsState(currentState);
11769
+ const userMessage = getTurnUserMessage(conversation, stored.resumeSessionId);
11770
+ if (!userMessage?.author?.userId) {
11771
+ return false;
11772
+ }
11773
+ if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
11774
+ return true;
11775
+ }
11776
+ const conversationContext = await buildCheckpointConversationContext(
11777
+ stored.resumeConversationId,
11778
+ stored.resumeSessionId
11779
+ );
11780
+ const channelConfiguration = getChannelConfigurationServiceById(
11781
+ stored.channelId
11782
+ );
11783
+ const providerLabel = formatProviderLabel(stored.provider);
11784
+ await resumeSlackTurn({
11785
+ messageText: stored.pendingMessage ?? userMessage.text,
11786
+ channelId: stored.channelId,
11787
+ threadTs: stored.threadTs,
11788
+ lockKey: stored.resumeConversationId,
11789
+ initialText: `Your ${providerLabel} account is now connected. Processing your request...`,
11790
+ failureText: "I connected your account but hit an error processing your request. Please try the command again.",
11791
+ replyContext: {
11792
+ assistant: { userName: botConfig.userName },
11793
+ requester: {
11794
+ userId: userMessage.author.userId,
11795
+ userName: userMessage.author.userName,
11796
+ fullName: userMessage.author.fullName
11797
+ },
11798
+ correlation: {
11799
+ channelId: stored.channelId,
11800
+ threadTs: stored.threadTs,
11801
+ requesterId: userMessage.author.userId
11802
+ },
11803
+ toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
11804
+ artifactState: artifacts,
11805
+ conversationContext,
11806
+ channelConfiguration,
11807
+ sandbox: getPersistedSandboxState(currentState),
11808
+ threadParticipants: buildThreadParticipants(conversation.messages),
11809
+ ...getTurnUserReplyAttachmentContext(userMessage)
11810
+ },
11811
+ onSuccess: async (reply) => {
11812
+ logInfo(
11813
+ "oauth_callback_resume_complete",
11814
+ {},
11815
+ {
11816
+ "app.credential.provider": stored.provider,
11817
+ "app.ai.outcome": reply.diagnostics.outcome,
11818
+ "app.ai.tool_calls": reply.diagnostics.toolCalls.length
11819
+ },
11820
+ "Auto-resumed checkpointed turn after OAuth callback"
11821
+ );
11822
+ await persistCompletedOAuthReplyState({
11823
+ conversationId: stored.resumeConversationId,
11824
+ sessionId: stored.resumeSessionId,
11825
+ reply
11826
+ });
11827
+ },
11828
+ onFailure: async (error) => {
11829
+ logException(
11830
+ error,
11831
+ "oauth_callback_resume_failed",
11832
+ {},
11833
+ { "app.credential.provider": stored.provider },
11834
+ "Failed to auto-resume checkpointed turn after OAuth callback"
11835
+ );
11836
+ await persistFailedOAuthReplyState({
11837
+ conversationId: stored.resumeConversationId,
11838
+ sessionId: stored.resumeSessionId
11839
+ });
11840
+ },
11841
+ onAuthPause: async (error) => {
11842
+ logException(
11843
+ error,
11844
+ "oauth_callback_resume_reparked_for_auth",
11845
+ {},
11846
+ { "app.credential.provider": stored.provider },
11847
+ "Resumed OAuth turn requested another authorization flow"
11848
+ );
11849
+ },
11850
+ onTimeoutPause: async (error) => {
11851
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
11852
+ throw error;
11853
+ }
11854
+ const checkpointVersion = error.metadata?.checkpointVersion;
11855
+ const nextSliceId = error.metadata?.sliceId;
11856
+ if (typeof checkpointVersion !== "number") {
11857
+ throw new Error(
11858
+ "Timed-out OAuth resume did not include a checkpoint version"
11859
+ );
11860
+ }
11861
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
11862
+ throw new Error(
11863
+ "Timed-out turn exceeded the automatic resume slice limit"
11864
+ );
11865
+ }
11866
+ await scheduleTurnTimeoutResume({
11867
+ conversationId: stored.resumeConversationId,
11868
+ sessionId: stored.resumeSessionId,
11869
+ expectedCheckpointVersion: checkpointVersion
11870
+ });
11871
+ }
11872
+ });
11873
+ return true;
11874
+ }
11883
11875
  async function resumePendingOAuthMessage(stored) {
11884
11876
  if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
11885
11877
  const providerLabel = formatProviderLabel(stored.provider);
@@ -12058,7 +12050,19 @@ async function GET5(request, provider, waitUntil) {
12058
12050
  }
12059
12051
  });
12060
12052
  if (stored.pendingMessage && stored.channelId && stored.threadTs) {
12061
- waitUntil(() => resumePendingOAuthMessage(stored));
12053
+ waitUntil(async () => {
12054
+ try {
12055
+ const resumed = await resumeCheckpointedOAuthTurn(stored);
12056
+ if (!resumed) {
12057
+ await resumePendingOAuthMessage(stored);
12058
+ }
12059
+ } catch (error) {
12060
+ if (error instanceof ResumeTurnBusyError) {
12061
+ return;
12062
+ }
12063
+ throw error;
12064
+ }
12065
+ });
12062
12066
  } else if (stored.channelId && stored.threadTs) {
12063
12067
  const { channelId, threadTs } = stored;
12064
12068
  waitUntil(
@@ -12382,11 +12386,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
12382
12386
  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;
12383
12387
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
12384
12388
  var RECENT_THREAD_WINDOW = 6;
12385
- function escapeRegExp2(value) {
12389
+ function escapeRegExp(value) {
12386
12390
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12387
12391
  }
12388
12392
  function containsAssistantInvocation(text, botUserName) {
12389
- const escapedUserName = escapeRegExp2(botUserName);
12393
+ const escapedUserName = escapeRegExp(botUserName);
12390
12394
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
12391
12395
  const labeledEntityMentionRe = new RegExp(
12392
12396
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -12605,6 +12609,13 @@ async function decideSubscribedThreadReply(args) {
12605
12609
  reason: "explicit_mention" /* ExplicitMention */
12606
12610
  };
12607
12611
  }
12612
+ if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification)) {
12613
+ return {
12614
+ shouldReply: true,
12615
+ reason: "directed_follow_up" /* DirectedFollowUp */,
12616
+ reasonDetail: signals.currentMessageIsTerseClarification ? "immediate terse clarification" : "immediate directed follow-up cue"
12617
+ };
12618
+ }
12608
12619
  if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
12609
12620
  return {
12610
12621
  shouldReply: false,
@@ -12675,7 +12686,7 @@ async function decideSubscribedThreadReply(args) {
12675
12686
  }
12676
12687
 
12677
12688
  // src/chat/runtime/thread-context.ts
12678
- function escapeRegExp3(value) {
12689
+ function escapeRegExp2(value) {
12679
12690
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12680
12691
  }
12681
12692
  function stripLeadingBotMention(text, options = {}) {
@@ -12685,12 +12696,12 @@ function stripLeadingBotMention(text, options = {}) {
12685
12696
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
12686
12697
  }
12687
12698
  const mentionByNameRe = new RegExp(
12688
- `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
12699
+ `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
12689
12700
  "i"
12690
12701
  );
12691
12702
  next = next.replace(mentionByNameRe, "").trim();
12692
12703
  const mentionByLabeledEntityRe = new RegExp(
12693
- `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
12704
+ `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
12694
12705
  "i"
12695
12706
  );
12696
12707
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -12892,13 +12903,13 @@ function createSlackTurnRuntime(deps) {
12892
12903
  channelId: deps.getChannelId(thread, message),
12893
12904
  runId: deps.getRunId(thread, message)
12894
12905
  });
12895
- if (isRetryableTurnError(error, "mcp_auth_resume")) {
12906
+ if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
12896
12907
  deps.logException(
12897
12908
  error,
12898
12909
  "mention_handler_auth_pause",
12899
12910
  errorContext,
12900
12911
  { "app.turn.retryable_reason": error.reason },
12901
- "onNewMention parked turn for MCP auth resume"
12912
+ "onNewMention parked turn for auth resume"
12902
12913
  );
12903
12914
  return;
12904
12915
  }
@@ -13024,13 +13035,13 @@ function createSlackTurnRuntime(deps) {
13024
13035
  channelId: deps.getChannelId(thread, message),
13025
13036
  runId: deps.getRunId(thread, message)
13026
13037
  });
13027
- if (isRetryableTurnError(error, "mcp_auth_resume")) {
13038
+ if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
13028
13039
  deps.logException(
13029
13040
  error,
13030
13041
  "subscribed_message_handler_auth_pause",
13031
13042
  errorContext,
13032
13043
  { "app.turn.retryable_reason": error.reason },
13033
- "onSubscribedMessage parked turn for MCP auth resume"
13044
+ "onSubscribedMessage parked turn for auth resume"
13034
13045
  );
13035
13046
  return;
13036
13047
  }
@@ -14211,7 +14222,7 @@ function createReplyToThread(deps) {
14211
14222
  );
14212
14223
  }
14213
14224
  } catch (error) {
14214
- if (isRetryableTurnError(error, "mcp_auth_resume")) {
14225
+ if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
14215
14226
  shouldPersistFailureState = false;
14216
14227
  throw error;
14217
14228
  }