@sentry/junior 0.26.0 → 0.27.1

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
@@ -25,7 +25,7 @@ import {
25
25
  sandboxSkillDir,
26
26
  sandboxSkillFile,
27
27
  toOptionalTrimmed
28
- } from "./chunk-A75TWGF2.js";
28
+ } from "./chunk-4PVJHUEV.js";
29
29
  import {
30
30
  CredentialUnavailableError,
31
31
  buildOAuthTokenRequest,
@@ -884,6 +884,9 @@ function mapSlackError(error) {
884
884
  if (apiError === "invalid_arguments") {
885
885
  return new SlackActionError(message, "invalid_arguments", baseOptions);
886
886
  }
887
+ if (apiError === "invalid_cursor") {
888
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
889
+ }
887
890
  if (apiError === "invalid_name") {
888
891
  return new SlackActionError(message, "invalid_arguments", baseOptions);
889
892
  }
@@ -2396,20 +2399,23 @@ function buildConversationContext(conversation, options = {}) {
2396
2399
  lines.push("<thread-compactions>");
2397
2400
  for (const [index, compaction] of conversation.compactions.entries()) {
2398
2401
  lines.push(
2399
- [
2400
- `summary_${index + 1}:`,
2401
- compaction.summary,
2402
- `covered_messages: ${compaction.coveredMessageIds.length}`,
2403
- `created_at: ${new Date(compaction.createdAtMs).toISOString()}`
2404
- ].join(" ")
2402
+ ` <compaction index="${index + 1}" covered_messages="${compaction.coveredMessageIds.length}" created_at="${new Date(compaction.createdAtMs).toISOString()}">`,
2403
+ compaction.summary,
2404
+ " </compaction>"
2405
2405
  );
2406
2406
  }
2407
- lines.push("</thread-compactions>");
2408
- lines.push("");
2407
+ lines.push("</thread-compactions>", "");
2409
2408
  }
2410
2409
  lines.push("<thread-transcript>");
2411
- for (const message of messages) {
2412
- lines.push(renderConversationMessageLine(message, conversation));
2410
+ for (const [index, message] of messages.entries()) {
2411
+ const author = escapeXml(message.author?.userName ?? message.role);
2412
+ const ts = new Date(message.createdAtMs).toISOString();
2413
+ const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
2414
+ lines.push(
2415
+ ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${slackTsAttr}>`,
2416
+ renderConversationMessageLine(message, conversation),
2417
+ " </message>"
2418
+ );
2413
2419
  }
2414
2420
  lines.push("</thread-transcript>");
2415
2421
  return lines.join("\n");
@@ -2441,9 +2447,14 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
2441
2447
  role: "user",
2442
2448
  content: [
2443
2449
  "Summarize the following older Slack thread transcript segment for future assistant turns.",
2444
- "Keep the summary factual and concise.",
2445
- "Preserve decisions, commitments, constraints, locations, hiring criteria, and unresolved asks.",
2446
- "Do not invent details.",
2450
+ "Keep the summary factual and concise. Do not invent details.",
2451
+ "",
2452
+ "Output exactly three XML sections in this order:",
2453
+ "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or superseded by a later turn. Omit the section body if none. </active-asks>",
2454
+ "<superseded-or-completed-asks> one bullet per ask that has been rescoped, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </superseded-or-completed-asks>",
2455
+ "<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
2456
+ "",
2457
+ "Do not output any text outside the three sections.",
2447
2458
  "",
2448
2459
  transcript
2449
2460
  ].join("\n"),
@@ -2690,135 +2701,6 @@ function truncateStatusText(text) {
2690
2701
  }
2691
2702
  return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
2692
2703
  }
2693
- function compactStatusPath(value) {
2694
- if (typeof value !== "string") {
2695
- return void 0;
2696
- }
2697
- const trimmed = value.trim();
2698
- if (!trimmed) {
2699
- return void 0;
2700
- }
2701
- if (trimmed.length <= 80) {
2702
- return trimmed;
2703
- }
2704
- return `...${trimmed.slice(-77)}`;
2705
- }
2706
- function compactStatusText(value, maxLength = 80) {
2707
- if (typeof value !== "string") {
2708
- return void 0;
2709
- }
2710
- const trimmed = value.trim();
2711
- if (!trimmed) {
2712
- return void 0;
2713
- }
2714
- return truncateWithEllipsis(trimmed, maxLength);
2715
- }
2716
- function readShellToken(command, startIndex) {
2717
- let index = startIndex;
2718
- while (index < command.length && /\s/.test(command[index] ?? "")) {
2719
- index += 1;
2720
- }
2721
- if (index >= command.length) {
2722
- return void 0;
2723
- }
2724
- let token = "";
2725
- let quote;
2726
- while (index < command.length) {
2727
- const char = command[index];
2728
- if (!char) {
2729
- break;
2730
- }
2731
- if (quote) {
2732
- if (char === quote) {
2733
- quote = void 0;
2734
- index += 1;
2735
- continue;
2736
- }
2737
- if (char === "\\" && quote === '"' && index + 1 < command.length) {
2738
- token += command[index + 1];
2739
- index += 2;
2740
- continue;
2741
- }
2742
- token += char;
2743
- index += 1;
2744
- continue;
2745
- }
2746
- if (/\s/.test(char)) {
2747
- break;
2748
- }
2749
- if (char === '"' || char === "'") {
2750
- quote = char;
2751
- index += 1;
2752
- continue;
2753
- }
2754
- if (char === "\\" && index + 1 < command.length) {
2755
- token += command[index + 1];
2756
- index += 2;
2757
- continue;
2758
- }
2759
- token += char;
2760
- index += 1;
2761
- }
2762
- return { token, nextIndex: index };
2763
- }
2764
- function compactStatusCommand(value) {
2765
- if (typeof value !== "string") {
2766
- return void 0;
2767
- }
2768
- const trimmed = value.trim();
2769
- if (!trimmed) {
2770
- return void 0;
2771
- }
2772
- let index = 0;
2773
- while (index < trimmed.length) {
2774
- const parsed = readShellToken(trimmed, index);
2775
- if (!parsed) {
2776
- return void 0;
2777
- }
2778
- index = parsed.nextIndex;
2779
- if (!parsed.token) {
2780
- continue;
2781
- }
2782
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
2783
- continue;
2784
- }
2785
- const normalized = parsed.token.replace(/[\\/]+$/g, "");
2786
- if (!normalized) {
2787
- return void 0;
2788
- }
2789
- const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
2790
- const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
2791
- return compactStatusText(command, 40);
2792
- }
2793
- return void 0;
2794
- }
2795
- function compactStatusFilename(value) {
2796
- if (typeof value !== "string") {
2797
- return void 0;
2798
- }
2799
- const trimmed = value.trim().replace(/[\\/]+$/g, "");
2800
- if (!trimmed) {
2801
- return void 0;
2802
- }
2803
- const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
2804
- const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
2805
- return compactStatusText(filename, 80);
2806
- }
2807
- function extractStatusUrlDomain(value) {
2808
- if (typeof value !== "string") {
2809
- return void 0;
2810
- }
2811
- const trimmed = value.trim();
2812
- if (!trimmed) {
2813
- return void 0;
2814
- }
2815
- try {
2816
- const parsed = new URL(trimmed);
2817
- return parsed.hostname || void 0;
2818
- } catch {
2819
- return void 0;
2820
- }
2821
- }
2822
2704
 
2823
2705
  // src/chat/slack/mrkdwn.ts
2824
2706
  function ensureBlockSpacing(text) {
@@ -3472,7 +3354,7 @@ function buildSystemPrompt(params) {
3472
3354
  [
3473
3355
  "- For factual or external questions, run tools/skills first, then answer from evidence.",
3474
3356
  "- Use tool descriptions as the source of truth for when each tool should or should not be called.",
3475
- "- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
3357
+ "- Use `reportProgress` only for sparse, meaningful progress updates. Pass a short user-facing status message, and do not call it for every tool or small substep.",
3476
3358
  "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
3477
3359
  "- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
3478
3360
  "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
@@ -3482,22 +3364,11 @@ function buildSystemPrompt(params) {
3482
3364
  "- 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
3365
  "- 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.",
3484
3366
  "- 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.",
3485
- "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3486
- "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
3487
3367
  "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
3488
3368
  "- If `attachFile` fails, explain the failure and do not say the file was shared.",
3489
- "- Use `imageGenerate` when the user asks for image creation.",
3490
- "- `imageGenerate` returns generated image metadata, including `attachment_path` values you can pass to `attachFile` when the user should receive the image.",
3491
- "- Use `slackCanvasCreate` for long-form docs/specs and `slackCanvasUpdate` for doc follow-ups.",
3492
- "- `slackCanvasUpdate` targets the active artifact-context canvas automatically; do not ask the user for `canvas_id`.",
3493
3369
  "- When you create or update a Slack artifact in this turn (for example a canvas, list, posted message, or attached file), mention it explicitly in the final reply and include its link when the tool returned one.",
3494
- "- Use `slackListCreate`, `slackListAddItems`, and `slackListUpdateItem` for actionable task tracking.",
3495
- "- `slackListAddItems`, `slackListGetItems`, and `slackListUpdateItem` target the active artifact-context list automatically; do not ask the user for `list_id`.",
3496
- "- If the user explicitly asks to post/send/share/say/show/announce/broadcast in the channel (outside this thread), call `slackChannelPostMessage` with the requested text instead of only replying in-thread.",
3497
3370
  "- For explicit in-channel post requests, prefer no thread text reply after a successful channel post. A reaction-only acknowledgment is acceptable when useful.",
3498
- "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
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.",
3500
- "- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
3371
+ "- When the user explicitly asks for an emoji reaction instead of text, react and skip the text reply.",
3501
3372
  "- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
3502
3373
  "- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
3503
3374
  "- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
@@ -3508,10 +3379,7 @@ function buildSystemPrompt(params) {
3508
3379
  "- `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.",
3509
3380
  "- When your work is complete, provide the exact user-facing markdown response.",
3510
3381
  "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3511
- "- Prefer `webSearch` before `webFetch` when the user gave no URL.",
3512
3382
  "- Never call side-effecting tools when the user only asked for analysis or options.",
3513
- "- `loadSkill` activates MCP tools when the loaded skill exposes them. After loading, call them directly by name (for example `mcp__provider__tool_name`).",
3514
- "- `searchTools` searches active MCP tools exposed by currently loaded skills when you need to rediscover or filter them.",
3515
3383
  "- When the user asks for their conversation ID, trace ID, or a reference for Sentry lookup, use the IDs from `<session-context>` and `<turn-context>` in the user turn."
3516
3384
  ].join("\n")
3517
3385
  ),
@@ -3529,8 +3397,7 @@ function buildSystemPrompt(params) {
3529
3397
  "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3530
3398
  "- Load only the best matching skill first; do not load multiple skills upfront.",
3531
3399
  "- After `loadSkill`, use `skill_dir` as the root for any referenced files you read via `bash`.",
3532
- "- If a loaded skill exposes MCP tools, they are registered as callable tools after `loadSkill` returns. Call them directly by name.",
3533
- "- Use `searchTools` only when you need to rediscover or filter the currently exposed MCP tools.",
3400
+ "- If a loaded skill exposes MCP tools, they are registered as callable tools after `loadSkill` returns. Call them directly by name (for example `mcp__provider__tool_name`).",
3534
3401
  "- If no skill is a clear fit, continue with normal tool usage."
3535
3402
  ].join("\n")
3536
3403
  ),
@@ -5193,8 +5060,22 @@ function createReadFileTool() {
5193
5060
  });
5194
5061
  }
5195
5062
 
5196
- // src/chat/tools/skill/search-tools.ts
5063
+ // src/chat/tools/runtime/report-progress.ts
5197
5064
  import { Type as Type6 } from "@sinclair/typebox";
5065
+ function createReportProgressTool() {
5066
+ return tool({
5067
+ description: "Update assistant status with a short user-facing progress message. Use this sparingly for meaningful progress changes, not for every tool call or minor substep.",
5068
+ inputSchema: Type6.Object({
5069
+ message: Type6.String({
5070
+ minLength: 1,
5071
+ description: "Short user-facing progress message. The UI truncates it if needed."
5072
+ })
5073
+ })
5074
+ });
5075
+ }
5076
+
5077
+ // src/chat/tools/skill/search-tools.ts
5078
+ import { Type as Type7 } from "@sinclair/typebox";
5198
5079
 
5199
5080
  // src/chat/tools/skill/mcp-tool-summary.ts
5200
5081
  function summarizeInputSchema(schema) {
@@ -5226,20 +5107,20 @@ var MAX_LIMIT = 20;
5226
5107
  function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5227
5108
  return tool({
5228
5109
  description: "Search active MCP tools exposed by the currently loaded skills. Use when you need to rediscover or filter active tools.",
5229
- inputSchema: Type6.Object(
5110
+ inputSchema: Type7.Object(
5230
5111
  {
5231
- query: Type6.String({
5112
+ query: Type7.String({
5232
5113
  minLength: 1,
5233
5114
  description: "Search query for matching MCP tool names or descriptions."
5234
5115
  }),
5235
- provider: Type6.Optional(
5236
- Type6.String({
5116
+ provider: Type7.Optional(
5117
+ Type7.String({
5237
5118
  minLength: 1,
5238
5119
  description: "Optional MCP provider filter, for example notion or sentry."
5239
5120
  })
5240
5121
  ),
5241
- limit: Type6.Optional(
5242
- Type6.Integer({
5122
+ limit: Type7.Optional(
5123
+ Type7.Integer({
5243
5124
  minimum: 1,
5244
5125
  maximum: MAX_LIMIT,
5245
5126
  description: "Maximum number of matching tools to return."
@@ -5265,7 +5146,7 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5265
5146
  }
5266
5147
 
5267
5148
  // src/chat/tools/slack/channel-list-messages.ts
5268
- import { Type as Type7 } from "@sinclair/typebox";
5149
+ import { Type as Type8 } from "@sinclair/typebox";
5269
5150
 
5270
5151
  // src/chat/slack/channel.ts
5271
5152
  async function listChannelMessages(input) {
@@ -5354,39 +5235,39 @@ async function listThreadReplies(input) {
5354
5235
  function createSlackChannelListMessagesTool(context) {
5355
5236
  return tool({
5356
5237
  description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
5357
- inputSchema: Type7.Object({
5358
- limit: Type7.Optional(
5359
- Type7.Integer({
5238
+ inputSchema: Type8.Object({
5239
+ limit: Type8.Optional(
5240
+ Type8.Integer({
5360
5241
  minimum: 1,
5361
5242
  maximum: 1e3,
5362
5243
  description: "Maximum number of messages to return across pages."
5363
5244
  })
5364
5245
  ),
5365
- cursor: Type7.Optional(
5366
- Type7.String({
5246
+ cursor: Type8.Optional(
5247
+ Type8.String({
5367
5248
  minLength: 1,
5368
5249
  description: "Optional cursor to continue from a prior call."
5369
5250
  })
5370
5251
  ),
5371
- oldest: Type7.Optional(
5372
- Type7.String({
5252
+ oldest: Type8.Optional(
5253
+ Type8.String({
5373
5254
  minLength: 1,
5374
5255
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
5375
5256
  })
5376
5257
  ),
5377
- latest: Type7.Optional(
5378
- Type7.String({
5258
+ latest: Type8.Optional(
5259
+ Type8.String({
5379
5260
  minLength: 1,
5380
5261
  description: "Optional latest message timestamp (Slack ts) for range filtering."
5381
5262
  })
5382
5263
  ),
5383
- inclusive: Type7.Optional(
5384
- Type7.Boolean({
5264
+ inclusive: Type8.Optional(
5265
+ Type8.Boolean({
5385
5266
  description: "Whether oldest/latest bounds should be inclusive."
5386
5267
  })
5387
5268
  ),
5388
- max_pages: Type7.Optional(
5389
- Type7.Integer({
5269
+ max_pages: Type8.Optional(
5270
+ Type8.Integer({
5390
5271
  minimum: 1,
5391
5272
  maximum: 10,
5392
5273
  description: "Maximum number of API pages to traverse in a single call."
@@ -5408,15 +5289,26 @@ function createSlackChannelListMessagesTool(context) {
5408
5289
  error: "No active channel context is available for history lookup"
5409
5290
  };
5410
5291
  }
5411
- const result = await listChannelMessages({
5412
- channelId: targetChannelId,
5413
- limit: limit ?? 100,
5414
- cursor,
5415
- oldest,
5416
- latest,
5417
- inclusive,
5418
- maxPages: max_pages
5419
- });
5292
+ let result;
5293
+ try {
5294
+ result = await listChannelMessages({
5295
+ channelId: targetChannelId,
5296
+ limit: limit ?? 100,
5297
+ cursor,
5298
+ oldest,
5299
+ latest,
5300
+ inclusive,
5301
+ maxPages: max_pages
5302
+ });
5303
+ } catch (error) {
5304
+ if (error instanceof SlackActionError && error.apiError === "invalid_cursor") {
5305
+ return {
5306
+ ok: false,
5307
+ error: "The supplied Slack history cursor is no longer valid. Retry the lookup without `cursor` to start from the newest page again."
5308
+ };
5309
+ }
5310
+ throw error;
5311
+ }
5420
5312
  return {
5421
5313
  ok: true,
5422
5314
  channel_id: targetChannelId,
@@ -5429,7 +5321,7 @@ function createSlackChannelListMessagesTool(context) {
5429
5321
  }
5430
5322
 
5431
5323
  // src/chat/tools/slack/channel-post-message.ts
5432
- import { Type as Type8 } from "@sinclair/typebox";
5324
+ import { Type as Type9 } from "@sinclair/typebox";
5433
5325
 
5434
5326
  // src/chat/tools/idempotency.ts
5435
5327
  function stableSerialize(value) {
@@ -5451,8 +5343,8 @@ function createOperationKey(toolName, input) {
5451
5343
  function createSlackChannelPostMessageTool(context, state) {
5452
5344
  return tool({
5453
5345
  description: "Post a message in the active Slack channel context (outside the thread). Use this when the user explicitly asks to post/send/share/say something in the channel. Do not use for normal thread replies or speculative broadcasts. Do not claim a channel message was posted unless this tool succeeds in this turn.",
5454
- inputSchema: Type8.Object({
5455
- text: Type8.String({
5346
+ inputSchema: Type9.Object({
5347
+ text: Type9.String({
5456
5348
  minLength: 1,
5457
5349
  maxLength: 4e4,
5458
5350
  description: "Slack mrkdwn text to post."
@@ -5495,12 +5387,12 @@ function createSlackChannelPostMessageTool(context, state) {
5495
5387
  }
5496
5388
 
5497
5389
  // src/chat/tools/slack/message-add-reaction.ts
5498
- import { Type as Type9 } from "@sinclair/typebox";
5390
+ import { Type as Type10 } from "@sinclair/typebox";
5499
5391
  function createSlackMessageAddReactionTool(context, state) {
5500
5392
  return tool({
5501
5393
  description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
5502
- inputSchema: Type9.Object({
5503
- emoji: Type9.String({
5394
+ inputSchema: Type10.Object({
5395
+ emoji: Type10.String({
5504
5396
  minLength: 1,
5505
5397
  maxLength: 64,
5506
5398
  description: "Slack emoji alias name to react with (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`). Optional surrounding colons are allowed."
@@ -5558,7 +5450,7 @@ function createSlackMessageAddReactionTool(context, state) {
5558
5450
  }
5559
5451
 
5560
5452
  // src/chat/tools/slack/canvas-tools.ts
5561
- import { Type as Type10 } from "@sinclair/typebox";
5453
+ import { Type as Type11 } from "@sinclair/typebox";
5562
5454
 
5563
5455
  // src/chat/tools/slack/canvases.ts
5564
5456
  function normalizeCanvasMarkdown(markdown) {
@@ -5577,19 +5469,11 @@ function normalizeCanvasMarkdown(markdown) {
5577
5469
  async function createCanvas(input) {
5578
5470
  const client2 = getSlackClient();
5579
5471
  const normalizedChannelId = normalizeSlackConversationId(input.channelId);
5580
- const isConversationScoped = isConversationScopedChannel(normalizedChannelId);
5581
- if (!isConversationScoped) {
5582
- throw new Error(
5583
- "Canvas creation requires an active Slack conversation context (C/G/D)."
5584
- );
5585
- }
5586
5472
  const channelPrefix = normalizedChannelId?.slice(0, 1) ?? "none";
5587
- const action = "conversations.canvases.create";
5588
5473
  const normalizedContent = normalizeCanvasMarkdown(input.markdown);
5589
5474
  const result = await withSlackRetries(
5590
5475
  async () => {
5591
- return client2.conversations.canvases.create({
5592
- channel_id: normalizedChannelId,
5476
+ return client2.canvases.create({
5593
5477
  title: input.title,
5594
5478
  document_content: {
5595
5479
  type: "markdown",
@@ -5599,7 +5483,7 @@ async function createCanvas(input) {
5599
5483
  },
5600
5484
  3,
5601
5485
  {
5602
- action,
5486
+ action: "canvases.create",
5603
5487
  attributes: {
5604
5488
  "app.slack.canvas.channel_id_prefix": channelPrefix,
5605
5489
  "app.slack.canvas.has_channel_id": Boolean(input.channelId),
@@ -5613,6 +5497,9 @@ async function createCanvas(input) {
5613
5497
  if (!result.canvas_id) {
5614
5498
  throw new Error("Slack canvas was created without canvas_id");
5615
5499
  }
5500
+ if (normalizedChannelId && isConversationScopedChannel(normalizedChannelId)) {
5501
+ await grantChannelCanvasAccess(result.canvas_id, normalizedChannelId);
5502
+ }
5616
5503
  let permalink;
5617
5504
  try {
5618
5505
  permalink = await getFilePermalink(result.canvas_id);
@@ -5623,6 +5510,39 @@ async function createCanvas(input) {
5623
5510
  permalink
5624
5511
  };
5625
5512
  }
5513
+ async function grantChannelCanvasAccess(canvasId, channelId) {
5514
+ const client2 = getSlackClient();
5515
+ try {
5516
+ await withSlackRetries(
5517
+ () => client2.canvases.access.set({
5518
+ canvas_id: canvasId,
5519
+ access_level: "write",
5520
+ channel_ids: [channelId]
5521
+ }),
5522
+ 3,
5523
+ {
5524
+ action: "canvases.access.set",
5525
+ attributes: {
5526
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
5527
+ "app.slack.canvas.channel_id_prefix": channelId.slice(0, 1),
5528
+ "app.slack.canvas.access_level": "write"
5529
+ }
5530
+ }
5531
+ );
5532
+ } catch (error) {
5533
+ logWarn(
5534
+ "slack_canvas_access_set_failed",
5535
+ {},
5536
+ {
5537
+ "app.slack.action": "canvases.access.set",
5538
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
5539
+ "app.slack.canvas.channel_id_prefix": channelId.slice(0, 1),
5540
+ "app.slack.canvas.access_level": "write"
5541
+ },
5542
+ error instanceof Error ? error.message : "Failed to grant channel access to canvas"
5543
+ );
5544
+ }
5545
+ }
5626
5546
  async function lookupCanvasSection(canvasId, containsText) {
5627
5547
  const client2 = getSlackClient();
5628
5548
  const response = await withSlackRetries(
@@ -5673,8 +5593,64 @@ async function updateCanvas(input) {
5673
5593
  }
5674
5594
  );
5675
5595
  }
5596
+ var CANVAS_ID_PATTERN = /^F[A-Z0-9]+$/i;
5597
+ var CANVAS_URL_FILE_ID_PATTERN = /\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]+)/i;
5598
+ function extractCanvasId(input) {
5599
+ const trimmed = input.trim();
5600
+ if (!trimmed) return void 0;
5601
+ if (CANVAS_ID_PATTERN.test(trimmed)) {
5602
+ return trimmed.toUpperCase();
5603
+ }
5604
+ const urlMatch = trimmed.match(CANVAS_URL_FILE_ID_PATTERN);
5605
+ if (urlMatch?.[1]) {
5606
+ return urlMatch[1].toUpperCase();
5607
+ }
5608
+ return void 0;
5609
+ }
5610
+ async function readCanvas(canvasIdOrUrl) {
5611
+ const canvasId = extractCanvasId(canvasIdOrUrl);
5612
+ if (!canvasId) {
5613
+ throw new Error(
5614
+ "Could not parse a Slack canvas/file ID from the provided input."
5615
+ );
5616
+ }
5617
+ const client2 = getSlackClient();
5618
+ const info = await withSlackRetries(
5619
+ () => client2.files.info({
5620
+ file: canvasId
5621
+ }),
5622
+ 3,
5623
+ {
5624
+ action: "files.info",
5625
+ attributes: {
5626
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
5627
+ }
5628
+ }
5629
+ );
5630
+ const file = info.file;
5631
+ if (!file) {
5632
+ throw new Error("Slack returned no file metadata for canvas.");
5633
+ }
5634
+ const downloadUrl = file.url_private_download ?? file.url_private;
5635
+ if (!downloadUrl) {
5636
+ throw new Error(
5637
+ "Canvas has no downloadable URL; bot token may lack file access."
5638
+ );
5639
+ }
5640
+ const buffer = await downloadPrivateSlackFile(downloadUrl);
5641
+ return {
5642
+ canvasId,
5643
+ title: file.title ?? file.name,
5644
+ permalink: file.permalink,
5645
+ mimetype: file.mimetype,
5646
+ filetype: file.filetype,
5647
+ content: buffer.toString("utf-8"),
5648
+ byteLength: buffer.byteLength
5649
+ };
5650
+ }
5676
5651
 
5677
5652
  // src/chat/tools/slack/canvas-tools.ts
5653
+ var MAX_CANVAS_READ_CHARS = 4e4;
5678
5654
  var MAX_RECENT_CANVASES = 5;
5679
5655
  function mergeRecentCanvases(existing, created) {
5680
5656
  const nextEntry = {
@@ -5690,13 +5666,13 @@ function mergeRecentCanvases(existing, created) {
5690
5666
  function createSlackCanvasCreateTool(context, state) {
5691
5667
  return tool({
5692
5668
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when content is too long for a thread reply or needs a persistent document. Do not use for short answers that fit in-thread.",
5693
- inputSchema: Type10.Object({
5694
- title: Type10.String({
5669
+ inputSchema: Type11.Object({
5670
+ title: Type11.String({
5695
5671
  minLength: 1,
5696
5672
  maxLength: 160,
5697
5673
  description: "Canvas title."
5698
5674
  }),
5699
- markdown: Type10.String({
5675
+ markdown: Type11.String({
5700
5676
  minLength: 1,
5701
5677
  description: "Canvas markdown body content."
5702
5678
  })
@@ -5762,29 +5738,29 @@ function createSlackCanvasCreateTool(context, state) {
5762
5738
  function createSlackCanvasUpdateTool(state, _context) {
5763
5739
  return tool({
5764
5740
  description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
5765
- inputSchema: Type10.Object({
5766
- markdown: Type10.String({
5741
+ inputSchema: Type11.Object({
5742
+ markdown: Type11.String({
5767
5743
  minLength: 1,
5768
5744
  description: "Markdown content to insert or use as replacement text."
5769
5745
  }),
5770
- operation: Type10.Optional(
5771
- Type10.Union(
5746
+ operation: Type11.Optional(
5747
+ Type11.Union(
5772
5748
  [
5773
- Type10.Literal("insert_at_end"),
5774
- Type10.Literal("insert_at_start"),
5775
- Type10.Literal("replace")
5749
+ Type11.Literal("insert_at_end"),
5750
+ Type11.Literal("insert_at_start"),
5751
+ Type11.Literal("replace")
5776
5752
  ],
5777
5753
  { description: "Canvas update mode." }
5778
5754
  )
5779
5755
  ),
5780
- section_id: Type10.Optional(
5781
- Type10.String({
5756
+ section_id: Type11.Optional(
5757
+ Type11.String({
5782
5758
  minLength: 1,
5783
5759
  description: "Optional section ID required for targeted replace operations."
5784
5760
  })
5785
5761
  ),
5786
- section_contains_text: Type10.Optional(
5787
- Type10.String({
5762
+ section_contains_text: Type11.Optional(
5763
+ Type11.String({
5788
5764
  minLength: 1,
5789
5765
  description: "Optional helper text used to find the target section when section_id is not provided."
5790
5766
  })
@@ -5847,9 +5823,61 @@ function createSlackCanvasUpdateTool(state, _context) {
5847
5823
  }
5848
5824
  });
5849
5825
  }
5826
+ function createSlackCanvasReadTool() {
5827
+ return tool({
5828
+ description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
5829
+ inputSchema: Type11.Object({
5830
+ canvas: Type11.String({
5831
+ minLength: 1,
5832
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
5833
+ })
5834
+ }),
5835
+ execute: async ({ canvas }) => {
5836
+ const canvasId = extractCanvasId(canvas);
5837
+ if (!canvasId) {
5838
+ return {
5839
+ ok: false,
5840
+ error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
5841
+ };
5842
+ }
5843
+ try {
5844
+ const result = await readCanvas(canvas);
5845
+ const truncated = result.content.length > MAX_CANVAS_READ_CHARS;
5846
+ const content = truncated ? result.content.slice(0, MAX_CANVAS_READ_CHARS) : result.content;
5847
+ return {
5848
+ ok: true,
5849
+ canvas_id: result.canvasId,
5850
+ title: result.title,
5851
+ permalink: result.permalink,
5852
+ mimetype: result.mimetype,
5853
+ filetype: result.filetype,
5854
+ original_byte_length: result.byteLength,
5855
+ truncated,
5856
+ content
5857
+ };
5858
+ } catch (error) {
5859
+ const message = error instanceof Error ? error.message : "canvas read failed";
5860
+ logWarn(
5861
+ "slack_canvas_read_failed",
5862
+ {},
5863
+ {
5864
+ "gen_ai.tool.name": "slackCanvasRead",
5865
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
5866
+ },
5867
+ message
5868
+ );
5869
+ return {
5870
+ ok: false,
5871
+ canvas_id: canvasId,
5872
+ error: message
5873
+ };
5874
+ }
5875
+ }
5876
+ });
5877
+ }
5850
5878
 
5851
5879
  // src/chat/tools/slack/list-tools.ts
5852
- import { Type as Type11 } from "@sinclair/typebox";
5880
+ import { Type as Type12 } from "@sinclair/typebox";
5853
5881
 
5854
5882
  // src/chat/tools/slack/lists.ts
5855
5883
  function normalizeKey(value) {
@@ -6023,8 +6051,8 @@ async function updateListItem(input) {
6023
6051
  function createSlackListCreateTool(state) {
6024
6052
  return tool({
6025
6053
  description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
6026
- inputSchema: Type11.Object({
6027
- name: Type11.String({
6054
+ inputSchema: Type12.Object({
6055
+ name: Type12.String({
6028
6056
  minLength: 1,
6029
6057
  maxLength: 160,
6030
6058
  description: "Name for the new Slack list."
@@ -6059,20 +6087,20 @@ function createSlackListCreateTool(state) {
6059
6087
  function createSlackListAddItemsTool(state) {
6060
6088
  return tool({
6061
6089
  description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
6062
- inputSchema: Type11.Object({
6063
- items: Type11.Array(Type11.String({ minLength: 1 }), {
6090
+ inputSchema: Type12.Object({
6091
+ items: Type12.Array(Type12.String({ minLength: 1 }), {
6064
6092
  minItems: 1,
6065
6093
  maxItems: 25,
6066
6094
  description: "List item titles to create."
6067
6095
  }),
6068
- assignee_user_id: Type11.Optional(
6069
- Type11.String({
6096
+ assignee_user_id: Type12.Optional(
6097
+ Type12.String({
6070
6098
  minLength: 1,
6071
6099
  description: "Optional Slack user ID assigned to all created items."
6072
6100
  })
6073
6101
  ),
6074
- due_date: Type11.Optional(
6075
- Type11.String({
6102
+ due_date: Type12.Optional(
6103
+ Type12.String({
6076
6104
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
6077
6105
  description: "Optional due date in YYYY-MM-DD format."
6078
6106
  })
@@ -6121,9 +6149,9 @@ function createSlackListAddItemsTool(state) {
6121
6149
  function createSlackListGetItemsTool(state) {
6122
6150
  return tool({
6123
6151
  description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
6124
- inputSchema: Type11.Object({
6125
- limit: Type11.Optional(
6126
- Type11.Integer({
6152
+ inputSchema: Type12.Object({
6153
+ limit: Type12.Optional(
6154
+ Type12.Integer({
6127
6155
  minimum: 1,
6128
6156
  maximum: 200,
6129
6157
  description: "Maximum number of list items to return."
@@ -6148,19 +6176,19 @@ function createSlackListGetItemsTool(state) {
6148
6176
  function createSlackListUpdateItemTool(state) {
6149
6177
  return tool({
6150
6178
  description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
6151
- inputSchema: Type11.Object(
6179
+ inputSchema: Type12.Object(
6152
6180
  {
6153
- item_id: Type11.String({
6181
+ item_id: Type12.String({
6154
6182
  minLength: 1,
6155
6183
  description: "ID of the Slack list item to update."
6156
6184
  }),
6157
- completed: Type11.Optional(
6158
- Type11.Boolean({
6185
+ completed: Type12.Optional(
6186
+ Type12.Boolean({
6159
6187
  description: "Optional completion status update."
6160
6188
  })
6161
6189
  ),
6162
- title: Type11.Optional(
6163
- Type11.String({
6190
+ title: Type12.Optional(
6191
+ Type12.String({
6164
6192
  minLength: 1,
6165
6193
  description: "Optional new item title."
6166
6194
  })
@@ -6210,11 +6238,11 @@ function createSlackListUpdateItemTool(state) {
6210
6238
  }
6211
6239
 
6212
6240
  // src/chat/tools/system-time.ts
6213
- import { Type as Type12 } from "@sinclair/typebox";
6241
+ import { Type as Type13 } from "@sinclair/typebox";
6214
6242
  function createSystemTimeTool() {
6215
6243
  return tool({
6216
6244
  description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
6217
- inputSchema: Type12.Object({}),
6245
+ inputSchema: Type13.Object({}),
6218
6246
  execute: async () => {
6219
6247
  const now = /* @__PURE__ */ new Date();
6220
6248
  return {
@@ -6229,7 +6257,7 @@ function createSystemTimeTool() {
6229
6257
  }
6230
6258
 
6231
6259
  // src/chat/tools/web/fetch-tool.ts
6232
- import { Type as Type13 } from "@sinclair/typebox";
6260
+ import { Type as Type14 } from "@sinclair/typebox";
6233
6261
 
6234
6262
  // src/chat/tools/web/constants.ts
6235
6263
  var USER_AGENT = "junior-bot/0.1";
@@ -6577,13 +6605,13 @@ function extractHttpStatusFromMessage(message) {
6577
6605
  function createWebFetchTool(hooks) {
6578
6606
  return tool({
6579
6607
  description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
6580
- inputSchema: Type13.Object({
6581
- url: Type13.String({
6608
+ inputSchema: Type14.Object({
6609
+ url: Type14.String({
6582
6610
  minLength: 1,
6583
6611
  description: "HTTP(S) URL to fetch."
6584
6612
  }),
6585
- max_chars: Type13.Optional(
6586
- Type13.Integer({
6613
+ max_chars: Type14.Optional(
6614
+ Type14.Integer({
6587
6615
  minimum: 500,
6588
6616
  maximum: MAX_FETCH_CHARS,
6589
6617
  description: "Optional maximum number of extracted characters to return."
@@ -6643,7 +6671,7 @@ function createWebFetchTool(hooks) {
6643
6671
  // src/chat/tools/web/search.ts
6644
6672
  import { generateText } from "ai";
6645
6673
  import { createGatewayProvider } from "@ai-sdk/gateway";
6646
- import { Type as Type14 } from "@sinclair/typebox";
6674
+ import { Type as Type15 } from "@sinclair/typebox";
6647
6675
  var SEARCH_TIMEOUT_MS = 6e4;
6648
6676
  var MAX_RESULTS = 5;
6649
6677
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
@@ -6686,14 +6714,14 @@ function isAuthFailure(message) {
6686
6714
  function createWebSearchTool() {
6687
6715
  return tool({
6688
6716
  description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
6689
- inputSchema: Type14.Object({
6690
- query: Type14.String({
6717
+ inputSchema: Type15.Object({
6718
+ query: Type15.String({
6691
6719
  minLength: 1,
6692
6720
  maxLength: 500,
6693
6721
  description: "Search query."
6694
6722
  }),
6695
- max_results: Type14.Optional(
6696
- Type14.Integer({
6723
+ max_results: Type15.Optional(
6724
+ Type15.Integer({
6697
6725
  minimum: 1,
6698
6726
  maximum: MAX_RESULTS,
6699
6727
  description: "Max results to return."
@@ -6759,17 +6787,17 @@ function createWebSearchTool() {
6759
6787
  }
6760
6788
 
6761
6789
  // src/chat/tools/sandbox/write-file.ts
6762
- import { Type as Type15 } from "@sinclair/typebox";
6790
+ import { Type as Type16 } from "@sinclair/typebox";
6763
6791
  function createWriteFileTool() {
6764
6792
  return tool({
6765
6793
  description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
6766
- inputSchema: Type15.Object(
6794
+ inputSchema: Type16.Object(
6767
6795
  {
6768
- path: Type15.String({
6796
+ path: Type16.String({
6769
6797
  minLength: 1,
6770
6798
  description: "Path to write in the sandbox workspace."
6771
6799
  }),
6772
- content: Type15.String({
6800
+ content: Type16.String({
6773
6801
  description: "UTF-8 file content to write."
6774
6802
  })
6775
6803
  },
@@ -6824,6 +6852,7 @@ function createTools(availableSkills, hooks = {}, context) {
6824
6852
  loadSkill: createLoadSkillTool(availableSkills, {
6825
6853
  onSkillLoaded: hooks.onSkillLoaded
6826
6854
  }),
6855
+ reportProgress: createReportProgressTool(),
6827
6856
  systemTime: createSystemTimeTool(),
6828
6857
  bash: createBashTool(),
6829
6858
  attachFile: createAttachFileTool(context.sandbox, hooks),
@@ -6835,6 +6864,7 @@ function createTools(availableSkills, hooks = {}, context) {
6835
6864
  hooks,
6836
6865
  hooks.toolOverrides?.imageGenerate
6837
6866
  ),
6867
+ slackCanvasRead: createSlackCanvasReadTool(),
6838
6868
  slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
6839
6869
  slackListCreate: createSlackListCreateTool(state),
6840
6870
  slackListAddItems: createSlackListAddItemsTool(state),
@@ -7084,346 +7114,9 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7084
7114
  import { Sandbox } from "@vercel/sandbox";
7085
7115
  import { createBashTool as createBashTool2 } from "bash-tool";
7086
7116
 
7087
- // src/chat/slack/assistant-thread/status-render.ts
7088
- var STATUS_PATTERNS = {
7089
- thinking: {
7090
- defaultContext: "\u2026",
7091
- variants: ["Thinking", "Reasoning", "Considering", "Working through"]
7092
- },
7093
- searching: {
7094
- defaultContext: "sources",
7095
- variants: ["Searching", "Scanning", "Probing", "Trawling"]
7096
- },
7097
- reading: {
7098
- defaultContext: "task",
7099
- variants: ["Reading", "Inspecting", "Parsing", "Skimming"]
7100
- },
7101
- reviewing: {
7102
- defaultContext: "results",
7103
- variants: ["Reviewing", "Checking", "Inspecting", "Auditing"]
7104
- },
7105
- loading: {
7106
- defaultContext: "task",
7107
- variants: ["Loading", "Priming", "Booting", "Spinning up"]
7108
- },
7109
- updating: {
7110
- defaultContext: "state",
7111
- variants: ["Updating", "Patching", "Refreshing", "Adjusting"]
7112
- },
7113
- fetching: {
7114
- defaultContext: "sources",
7115
- variants: ["Fetching", "Pulling", "Retrieving", "Loading"]
7116
- },
7117
- creating: {
7118
- defaultContext: "draft",
7119
- variants: ["Creating", "Building", "Assembling", "Generating"]
7120
- },
7121
- listing: {
7122
- defaultContext: "items",
7123
- variants: ["Listing", "Gathering", "Collecting", "Enumerating"]
7124
- },
7125
- posting: {
7126
- defaultContext: "reply",
7127
- variants: ["Posting", "Sending", "Delivering", "Dispatching"]
7128
- },
7129
- adding: {
7130
- defaultContext: "details",
7131
- variants: ["Adding", "Applying", "Attaching", "Dropping in"]
7132
- },
7133
- running: {
7134
- defaultContext: "tasks",
7135
- variants: ["Running", "Executing", "Launching", "Processing"]
7136
- }
7137
- };
7138
- function makeAssistantStatus(kind, context) {
7139
- return { kind, ...context ? { context } : {} };
7140
- }
7141
- function renderAssistantStatus(args) {
7142
- const random = args.random ?? Math.random;
7143
- const pattern = STATUS_PATTERNS[args.status.kind];
7144
- const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
7145
- const index = Math.floor(random() * pattern.variants.length);
7146
- const verb = pattern.variants[index] ?? pattern.variants[0];
7147
- const visible = truncateStatusText(`${verb} ${context}`);
7148
- const hint = truncateStatusText(`${pattern.variants[0]} ${context}`);
7149
- return {
7150
- key: `${args.status.kind}:${context}`,
7151
- hint,
7152
- visible,
7153
- suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7154
- };
7155
- }
7156
-
7157
- // src/chat/slack/assistant-thread/status-scheduler.ts
7158
- var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
7159
- var STATUS_MIN_VISIBLE_MS = 1200;
7160
- var STATUS_ROTATION_INTERVAL_MS = 3e4;
7161
- function createAssistantStatusScheduler(args) {
7162
- const now = args.now ?? (() => Date.now());
7163
- const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
7164
- const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
7165
- const random = args.random ?? Math.random;
7166
- let active = false;
7167
- let currentKey = "";
7168
- let currentStatus = makeAssistantStatus("thinking");
7169
- let currentVisibleStatus = "";
7170
- let lastStatusAt = 0;
7171
- let pendingStatus = null;
7172
- let pendingKey = "";
7173
- let pendingTimer = null;
7174
- let rotationTimer = null;
7175
- let inflightStatusUpdate = Promise.resolve();
7176
- const enqueueStatusUpdate = (task) => {
7177
- const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
7178
- await task();
7179
- });
7180
- inflightStatusUpdate = request.catch(() => void 0);
7181
- return request;
7182
- };
7183
- const scheduleRotation = () => {
7184
- if (rotationTimer) {
7185
- clearTimer(rotationTimer);
7186
- rotationTimer = null;
7187
- }
7188
- if (!active || !currentVisibleStatus) {
7189
- return;
7190
- }
7191
- rotationTimer = setTimer(() => {
7192
- rotationTimer = null;
7193
- if (!active || !currentVisibleStatus) {
7194
- return;
7195
- }
7196
- void postRenderedStatus(currentStatus);
7197
- }, STATUS_ROTATION_INTERVAL_MS);
7198
- };
7199
- const postStatus = async (text, suggestions) => {
7200
- if (!text && !currentVisibleStatus) {
7201
- return;
7202
- }
7203
- currentVisibleStatus = text;
7204
- lastStatusAt = now();
7205
- scheduleRotation();
7206
- await enqueueStatusUpdate(async () => {
7207
- await args.sendStatus(text, suggestions);
7208
- });
7209
- };
7210
- const postRenderedStatus = async (status) => {
7211
- const presentation = renderAssistantStatus({
7212
- status,
7213
- random
7214
- });
7215
- currentStatus = status;
7216
- currentKey = presentation.key;
7217
- await postStatus(presentation.visible, presentation.suggestions);
7218
- };
7219
- const clearPending = () => {
7220
- if (pendingTimer) {
7221
- clearTimer(pendingTimer);
7222
- pendingTimer = null;
7223
- }
7224
- pendingStatus = null;
7225
- pendingKey = "";
7226
- };
7227
- const flushPending = async () => {
7228
- if (!active || !pendingStatus) {
7229
- clearPending();
7230
- return;
7231
- }
7232
- const next = pendingStatus;
7233
- clearPending();
7234
- const nextPresentation = renderAssistantStatus({
7235
- status: next,
7236
- random
7237
- });
7238
- if (nextPresentation.key !== currentKey) {
7239
- await postRenderedStatus(next);
7240
- }
7241
- };
7242
- return {
7243
- start() {
7244
- active = true;
7245
- clearPending();
7246
- currentStatus = makeAssistantStatus("thinking");
7247
- currentKey = "";
7248
- void postRenderedStatus(currentStatus);
7249
- },
7250
- async stop() {
7251
- active = false;
7252
- clearPending();
7253
- if (rotationTimer) {
7254
- clearTimer(rotationTimer);
7255
- rotationTimer = null;
7256
- }
7257
- currentKey = "";
7258
- await postStatus("");
7259
- },
7260
- update(status) {
7261
- if (!active) {
7262
- return;
7263
- }
7264
- const presentation = renderAssistantStatus({
7265
- status,
7266
- random
7267
- });
7268
- if (!presentation.visible) {
7269
- return;
7270
- }
7271
- if (presentation.key === currentKey || presentation.key === pendingKey) {
7272
- return;
7273
- }
7274
- const elapsed = now() - lastStatusAt;
7275
- const waitMs = Math.max(
7276
- STATUS_UPDATE_DEBOUNCE_MS - elapsed,
7277
- STATUS_MIN_VISIBLE_MS - elapsed,
7278
- 0
7279
- );
7280
- if (waitMs <= 0) {
7281
- clearPending();
7282
- void postRenderedStatus(status);
7283
- return;
7284
- }
7285
- pendingStatus = status;
7286
- pendingKey = presentation.key;
7287
- if (pendingTimer) {
7288
- return;
7289
- }
7290
- pendingTimer = setTimer(
7291
- () => {
7292
- pendingTimer = null;
7293
- void flushPending();
7294
- },
7295
- Math.max(1, waitMs)
7296
- );
7297
- }
7298
- };
7299
- }
7300
-
7301
- // src/chat/slack/assistant-thread/status-send.ts
7302
- function createSlackAdapterStatusSender(args) {
7303
- const adapter = args.getSlackAdapter();
7304
- const boundToken = getSlackAdapterRequestToken(adapter);
7305
- return async (text, suggestions) => {
7306
- const channelId = args.channelId;
7307
- const threadTs = args.threadTs;
7308
- if (!channelId || !threadTs) {
7309
- return;
7310
- }
7311
- const normalizedChannelId = normalizeSlackConversationId(channelId);
7312
- if (!normalizedChannelId) {
7313
- return;
7314
- }
7315
- try {
7316
- await runWithBoundSlackToken(
7317
- adapter,
7318
- boundToken,
7319
- () => adapter.setAssistantStatus(
7320
- normalizedChannelId,
7321
- threadTs,
7322
- text,
7323
- suggestions
7324
- )
7325
- );
7326
- } catch (error) {
7327
- logAssistantStatusFailure({
7328
- status: text,
7329
- error,
7330
- channelId,
7331
- normalizedChannelId,
7332
- threadTs
7333
- });
7334
- }
7335
- };
7336
- }
7337
- function createSlackWebApiStatusSender(args) {
7338
- const getClient2 = args.getSlackClient ?? getSlackClient;
7339
- return async (text, suggestions) => {
7340
- const channelId = args.channelId;
7341
- const threadTs = args.threadTs;
7342
- if (!channelId || !threadTs) {
7343
- return;
7344
- }
7345
- const normalizedChannelId = normalizeSlackConversationId(channelId);
7346
- if (!normalizedChannelId) {
7347
- return;
7348
- }
7349
- try {
7350
- await getClient2().assistant.threads.setStatus({
7351
- channel_id: normalizedChannelId,
7352
- thread_ts: threadTs,
7353
- status: text,
7354
- ...suggestions ? { loading_messages: suggestions } : {}
7355
- });
7356
- } catch (error) {
7357
- logAssistantStatusFailure({
7358
- status: text,
7359
- error,
7360
- channelId,
7361
- normalizedChannelId,
7362
- threadTs
7363
- });
7364
- }
7365
- };
7366
- }
7367
- function getSlackAdapterRequestToken(adapter) {
7368
- const token = adapter.requestContext?.getStore()?.token;
7369
- if (typeof token !== "string") {
7370
- return void 0;
7371
- }
7372
- const trimmed = token.trim();
7373
- return trimmed || void 0;
7374
- }
7375
- async function runWithBoundSlackToken(adapter, token, task) {
7376
- if (!token) {
7377
- return await task();
7378
- }
7379
- return await adapter.withBotToken(token, task);
7380
- }
7381
- function logAssistantStatusFailure(args) {
7382
- logWarn(
7383
- "assistant_status_update_failed",
7384
- {},
7385
- {
7386
- "app.slack.status_text": args.status || "(clear)",
7387
- "app.slack.channel_id_raw": args.channelId,
7388
- "app.slack.channel_id": args.normalizedChannelId,
7389
- "app.slack.thread_ts": args.threadTs,
7390
- "error.message": args.error instanceof Error ? args.error.message : String(args.error)
7391
- },
7392
- `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
7393
- );
7394
- }
7395
-
7396
- // src/chat/slack/assistant-thread/status.ts
7397
- function createSlackAdapterAssistantStatusSession(args) {
7398
- return createAssistantStatusScheduler({
7399
- sendStatus: createSlackAdapterStatusSender({
7400
- channelId: args.channelId,
7401
- threadTs: args.threadTs,
7402
- getSlackAdapter: args.getSlackAdapter
7403
- }),
7404
- now: args.now,
7405
- setTimer: args.setTimer,
7406
- clearTimer: args.clearTimer,
7407
- random: args.random
7408
- });
7409
- }
7410
- function createSlackWebApiAssistantStatusSession(args) {
7411
- return createAssistantStatusScheduler({
7412
- sendStatus: createSlackWebApiStatusSender({
7413
- channelId: args.channelId,
7414
- threadTs: args.threadTs,
7415
- getSlackClient: args.getSlackClient
7416
- }),
7417
- now: args.now,
7418
- setTimer: args.setTimer,
7419
- clearTimer: args.clearTimer,
7420
- random: args.random
7421
- });
7422
- }
7423
-
7424
- // src/chat/sandbox/skill-sync.ts
7425
- import fs3 from "fs/promises";
7426
- import path5 from "path";
7117
+ // src/chat/sandbox/skill-sync.ts
7118
+ import fs3 from "fs/promises";
7119
+ import path5 from "path";
7427
7120
 
7428
7121
  // src/chat/sandbox/eval-gh-stub.ts
7429
7122
  function buildEvalGitHubCliStub() {
@@ -7981,12 +7674,6 @@ var SANDBOX_RUNTIME = "node22";
7981
7674
  var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
7982
7675
  var SNAPSHOT_BOOT_RETRY_COUNT = 3;
7983
7676
  var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
7984
- var SNAPSHOT_PHASE_STATUS = {
7985
- resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
7986
- waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
7987
- building_snapshot: { kind: "creating", context: "sandbox snapshot" },
7988
- cache_hit: { kind: "loading", context: "sandbox snapshot" }
7989
- };
7990
7677
  function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
7991
7678
  const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
7992
7679
  const existingAllowRaw = basePolicy.allow;
@@ -8027,26 +7714,6 @@ function sleep2(ms) {
8027
7714
  setTimeout(resolve, ms);
8028
7715
  });
8029
7716
  }
8030
- function createStatusEmitter(emitStatus) {
8031
- let statusCount = 0;
8032
- const sentStatuses = /* @__PURE__ */ new Set();
8033
- const emit = async (status) => {
8034
- const statusKey = `${status.kind}:${status.context ?? ""}`;
8035
- if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
8036
- return;
8037
- }
8038
- sentStatuses.add(statusKey);
8039
- statusCount += 1;
8040
- await emitStatus(status);
8041
- };
8042
- const reportSnapshotPhase = async (phase) => {
8043
- const status = SNAPSHOT_PHASE_STATUS[phase];
8044
- if (status) {
8045
- await emit(makeAssistantStatus(status.kind, status.context));
8046
- }
8047
- };
8048
- return { emit, reportSnapshotPhase };
8049
- }
8050
7717
  function parseKeepAliveMs() {
8051
7718
  const parsed = Number.parseInt(
8052
7719
  process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
@@ -8064,23 +7731,6 @@ function createSandboxSessionManager(options) {
8064
7731
  const traceContext = options?.traceContext ?? {};
8065
7732
  const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
8066
7733
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8067
- const emitSandboxStatus = async (source, statusEmitter, status) => {
8068
- logInfo(
8069
- "sandbox_status_emitted",
8070
- traceContext,
8071
- {
8072
- "app.sandbox.status.source": source,
8073
- "app.sandbox.status.kind": status.kind,
8074
- ...status.context ? { "app.sandbox.status.context": status.context } : {}
8075
- },
8076
- "Sandbox status emitted"
8077
- );
8078
- if (typeof statusEmitter === "function") {
8079
- await statusEmitter(status);
8080
- return;
8081
- }
8082
- await statusEmitter.emit(status);
8083
- };
8084
7734
  const clearSession = () => {
8085
7735
  sandbox = null;
8086
7736
  sandboxIdHint = void 0;
@@ -8156,16 +7806,9 @@ function createSandboxSessionManager(options) {
8156
7806
  });
8157
7807
  return replacement;
8158
7808
  };
8159
- const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
7809
+ const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials) => {
8160
7810
  for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
8161
7811
  try {
8162
- if (emitStatus) {
8163
- await emitSandboxStatus(
8164
- "snapshot_boot",
8165
- emitStatus,
8166
- makeAssistantStatus("loading", "sandbox")
8167
- );
8168
- }
8169
7812
  return await Sandbox.create({
8170
7813
  timeout: timeoutMs,
8171
7814
  source: {
@@ -8198,13 +7841,8 @@ function createSandboxSessionManager(options) {
8198
7841
  });
8199
7842
  };
8200
7843
  const createSandboxFromResolvedSnapshot = async (params) => {
8201
- const { runtime, snapshot, sandboxCredentials, status } = params;
7844
+ const { runtime, snapshot, sandboxCredentials } = params;
8202
7845
  if (!snapshot.snapshotId) {
8203
- await emitSandboxStatus(
8204
- "fresh_runtime_boot",
8205
- status,
8206
- makeAssistantStatus("loading", "sandbox")
8207
- );
8208
7846
  return await Sandbox.create({
8209
7847
  timeout: timeoutMs,
8210
7848
  runtime,
@@ -8214,8 +7852,7 @@ function createSandboxSessionManager(options) {
8214
7852
  try {
8215
7853
  return await createSandboxFromSnapshot(
8216
7854
  snapshot.snapshotId,
8217
- sandboxCredentials,
8218
- status.emit
7855
+ sandboxCredentials
8219
7856
  );
8220
7857
  } catch (error) {
8221
7858
  if (!isSnapshotMissingError(error)) {
@@ -8228,23 +7865,20 @@ function createSandboxSessionManager(options) {
8228
7865
  runtime,
8229
7866
  timeoutMs,
8230
7867
  forceRebuild: true,
8231
- staleSnapshotId: snapshot.snapshotId,
8232
- onProgress: status.reportSnapshotPhase
7868
+ staleSnapshotId: snapshot.snapshotId
8233
7869
  });
8234
7870
  if (!rebuiltSnapshot.snapshotId) {
8235
7871
  throw error;
8236
7872
  }
8237
7873
  return await createSandboxFromSnapshot(
8238
7874
  rebuiltSnapshot.snapshotId,
8239
- sandboxCredentials,
8240
- status.emit
7875
+ sandboxCredentials
8241
7876
  );
8242
7877
  }
8243
7878
  };
8244
7879
  const createFreshSandbox = async () => {
8245
7880
  const runtime = SANDBOX_RUNTIME;
8246
7881
  const sandboxCredentials = getVercelSandboxCredentials();
8247
- const status = createStatusEmitter(options?.onStatus);
8248
7882
  let createdSandbox;
8249
7883
  try {
8250
7884
  createdSandbox = await withSandboxSpan(
@@ -8256,22 +7890,15 @@ function createSandboxSessionManager(options) {
8256
7890
  "app.sandbox.runtime": runtime
8257
7891
  },
8258
7892
  async () => {
8259
- await emitSandboxStatus(
8260
- "runtime_dependency_resolve",
8261
- status,
8262
- makeAssistantStatus("loading", "sandbox runtime")
8263
- );
8264
7893
  const snapshot = await resolveRuntimeDependencySnapshot({
8265
7894
  runtime,
8266
- timeoutMs,
8267
- onProgress: status.reportSnapshotPhase
7895
+ timeoutMs
8268
7896
  });
8269
7897
  setSnapshotAttributes(snapshot);
8270
7898
  return await createSandboxFromResolvedSnapshot({
8271
7899
  runtime,
8272
7900
  snapshot,
8273
- sandboxCredentials,
8274
- status
7901
+ sandboxCredentials
8275
7902
  });
8276
7903
  }
8277
7904
  );
@@ -8585,7 +8212,6 @@ function createSandboxExecutor(options) {
8585
8212
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8586
8213
  timeoutMs: options?.timeoutMs,
8587
8214
  traceContext,
8588
- onStatus: options?.onStatus,
8589
8215
  onSandboxAcquired: options?.onSandboxAcquired
8590
8216
  });
8591
8217
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
@@ -8938,74 +8564,20 @@ function createPluginAuthOrchestration(deps, abortAgent) {
8938
8564
  };
8939
8565
  }
8940
8566
 
8941
- // src/chat/runtime/tool-status.ts
8942
- function buildToolStatus(toolName, input) {
8943
- const obj = input && typeof input === "object" ? input : void 0;
8944
- const command = obj ? compactStatusCommand(obj.command) : void 0;
8945
- const path7 = obj ? compactStatusPath(obj.path) : void 0;
8946
- const filename = obj ? compactStatusFilename(obj.path) : void 0;
8947
- const query = obj ? compactStatusText(obj.query, 70) : void 0;
8948
- const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
8949
- const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
8950
- const provider = obj ? compactStatusText(obj.provider, 20) : void 0;
8951
- if (command && toolName === "bash") {
8952
- return makeAssistantStatus("running", command);
8953
- }
8954
- if (filename && toolName === "readFile") {
8955
- return makeAssistantStatus("reading", filename);
8956
- }
8957
- if (filename && toolName === "writeFile") {
8958
- return makeAssistantStatus("updating", filename);
8959
- }
8960
- if (path7 && toolName === "writeFile") {
8961
- return makeAssistantStatus("updating", path7);
8962
- }
8963
- if (skillName && toolName === "loadSkill") {
8964
- return makeAssistantStatus("loading", skillName);
8965
- }
8966
- if (query && toolName === "webSearch") {
8967
- return makeAssistantStatus("searching", "sources");
8968
- }
8969
- if (query && toolName === "searchTools") {
8970
- return makeAssistantStatus(
8971
- "searching",
8972
- provider ? `${provider} tools` : "tools"
8973
- );
8974
- }
8975
- if (domain && toolName === "webFetch") {
8976
- return makeAssistantStatus("fetching", domain);
8977
- }
8978
- const known = {
8979
- loadSkill: makeAssistantStatus("loading", "skill instructions"),
8980
- systemTime: makeAssistantStatus("reading", "system time"),
8981
- bash: makeAssistantStatus("running", "shell"),
8982
- readFile: makeAssistantStatus("reading", "file"),
8983
- writeFile: makeAssistantStatus("updating", "file"),
8984
- webSearch: makeAssistantStatus("searching", "sources"),
8985
- webFetch: makeAssistantStatus("fetching", "pages"),
8986
- slackChannelPostMessage: makeAssistantStatus("posting", "channel"),
8987
- slackMessageAddReaction: makeAssistantStatus("adding", "reaction"),
8988
- slackChannelListMessages: makeAssistantStatus("listing", "messages"),
8989
- slackCanvasCreate: makeAssistantStatus("creating", "brief"),
8990
- slackCanvasUpdate: makeAssistantStatus("updating", "brief"),
8991
- slackListCreate: makeAssistantStatus("creating", "tracking list"),
8992
- slackListAddItems: makeAssistantStatus("updating", "tracking list"),
8993
- slackListUpdateItem: makeAssistantStatus("updating", "tracking list"),
8994
- imageGenerate: makeAssistantStatus("creating", "image"),
8995
- searchTools: makeAssistantStatus(
8996
- "searching",
8997
- provider ? `${provider} tools` : "tools"
8998
- )
8999
- };
9000
- if (known[toolName]) {
9001
- return known[toolName];
8567
+ // src/chat/runtime/report-progress.ts
8568
+ function buildReportedProgressStatus(input) {
8569
+ if (!input || typeof input !== "object") {
8570
+ return void 0;
9002
8571
  }
9003
- const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(toolName);
9004
- if (mcpMatch) {
9005
- return makeAssistantStatus("running", `${mcpMatch[1]}/${mcpMatch[2]}`);
8572
+ const message = input.message;
8573
+ if (typeof message !== "string") {
8574
+ return void 0;
8575
+ }
8576
+ const text = message.trim();
8577
+ if (!text) {
8578
+ return void 0;
9006
8579
  }
9007
- const readable = toolName.replaceAll("_", " ").trim();
9008
- return makeAssistantStatus("running", readable || "tool");
8580
+ return { text };
9009
8581
  }
9010
8582
 
9011
8583
  // src/chat/tools/execution/build-sandbox-input.ts
@@ -9156,7 +8728,12 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9156
8728
  turnId: spanContext.turnId,
9157
8729
  agentId: spanContext.agentId
9158
8730
  };
9159
- await onStatus?.(buildToolStatus(toolName, params));
8731
+ if (toolName === "reportProgress") {
8732
+ const status = buildReportedProgressStatus(params);
8733
+ if (status) {
8734
+ await onStatus?.(status);
8735
+ }
8736
+ }
9160
8737
  return withSpan(
9161
8738
  `execute_tool ${toolName}`,
9162
8739
  "gen_ai.execute_tool",
@@ -9322,41 +8899,41 @@ function summarizeMessageText(text) {
9322
8899
  }
9323
8900
  function buildUserTurnText(userInput, conversationContext, metadata) {
9324
8901
  const trimmedContext = conversationContext?.trim();
9325
- const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
9326
- const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
9327
- if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
8902
+ const conversationId = metadata?.sessionContext?.conversationId;
8903
+ const traceId = metadata?.turnContext?.traceId;
8904
+ if (!trimmedContext && !conversationId && !traceId) {
9328
8905
  return userInput;
9329
8906
  }
9330
- const sections = [
9331
- "<current-message>",
9332
- userInput,
9333
- "</current-message>"
9334
- ];
8907
+ const sections = [];
9335
8908
  if (trimmedContext) {
9336
8909
  sections.push(
9337
- "",
9338
- "<thread-conversation-context>",
9339
- "Use this context for continuity across prior thread turns.",
8910
+ "<thread-background>",
9340
8911
  trimmedContext,
9341
- "</thread-conversation-context>"
8912
+ "</thread-background>",
8913
+ ""
9342
8914
  );
9343
8915
  }
9344
- if (metadata?.sessionContext?.conversationId) {
8916
+ if (conversationId) {
9345
8917
  sections.push(
9346
- "",
9347
8918
  "<session-context>",
9348
- `- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
9349
- "</session-context>"
8919
+ `- gen_ai.conversation.id: ${conversationId}`,
8920
+ "</session-context>",
8921
+ ""
9350
8922
  );
9351
8923
  }
9352
- if (metadata?.turnContext?.traceId) {
8924
+ if (traceId) {
9353
8925
  sections.push(
9354
- "",
9355
8926
  "<turn-context>",
9356
- `- trace_id: ${metadata.turnContext.traceId}`,
9357
- "</turn-context>"
8927
+ `- trace_id: ${traceId}`,
8928
+ "</turn-context>",
8929
+ ""
9358
8930
  );
9359
8931
  }
8932
+ sections.push(
8933
+ '<current-instruction priority="highest">',
8934
+ userInput,
8935
+ "</current-instruction>"
8936
+ );
9360
8937
  return sections.join("\n");
9361
8938
  }
9362
8939
  function encodeNonImageAttachmentForPrompt(attachment) {
@@ -10059,7 +9636,6 @@ async function generateAssistantReply(messageText, context = {}) {
10059
9636
  sandboxId: context.sandbox?.sandboxId,
10060
9637
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
10061
9638
  traceContext: spanContext,
10062
- onStatus: context.onStatus,
10063
9639
  onSandboxAcquired: async (sandbox2) => {
10064
9640
  lastKnownSandboxId = sandbox2.sandboxId;
10065
9641
  lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
@@ -10679,6 +10255,364 @@ async function generateAssistantReply(messageText, context = {}) {
10679
10255
  }
10680
10256
  }
10681
10257
 
10258
+ // src/chat/slack/assistant-thread/status-render.ts
10259
+ var DEFAULT_STATUS_CONTEXTS = {
10260
+ thinking: "\u2026",
10261
+ searching: "sources",
10262
+ reading: "task",
10263
+ reviewing: "results",
10264
+ drafting: "reply",
10265
+ running: "tasks"
10266
+ };
10267
+ function formatAssistantStatusText(verb, context) {
10268
+ const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
10269
+ const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
10270
+ if (!normalizedVerb) {
10271
+ return truncateStatusText(normalizedContext || "Working");
10272
+ }
10273
+ const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
10274
+ return truncateStatusText(
10275
+ normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb
10276
+ );
10277
+ }
10278
+ function makeAssistantStatus(verb, context) {
10279
+ return {
10280
+ text: formatAssistantStatusText(verb, context)
10281
+ };
10282
+ }
10283
+ function renderAssistantStatus(args) {
10284
+ const visible = truncateStatusText(
10285
+ normalizeSlackStatusText(args.status.text)
10286
+ );
10287
+ return {
10288
+ key: visible,
10289
+ visible
10290
+ };
10291
+ }
10292
+ function selectAssistantLoadingMessages(args) {
10293
+ const random = args.random ?? Math.random;
10294
+ const normalized = Array.from(
10295
+ new Set(
10296
+ args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)
10297
+ )
10298
+ );
10299
+ if (normalized.length === 0) {
10300
+ return void 0;
10301
+ }
10302
+ const shuffled = [...normalized];
10303
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
10304
+ const otherIndex = Math.floor(random() * (index + 1));
10305
+ [shuffled[index], shuffled[otherIndex]] = [
10306
+ shuffled[otherIndex],
10307
+ shuffled[index]
10308
+ ];
10309
+ }
10310
+ return shuffled.slice(0, 10);
10311
+ }
10312
+
10313
+ // src/chat/slack/assistant-thread/status-scheduler.ts
10314
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
10315
+ var STATUS_MIN_VISIBLE_MS = 1200;
10316
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
10317
+ function createAssistantStatusScheduler(args) {
10318
+ const now = args.now ?? (() => Date.now());
10319
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
10320
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
10321
+ const random = args.random ?? Math.random;
10322
+ const loadingMessages = selectAssistantLoadingMessages({
10323
+ messages: args.loadingMessages ?? [],
10324
+ random
10325
+ });
10326
+ const defaultStatus = makeAssistantStatus("thinking");
10327
+ let active = false;
10328
+ let currentKey = "";
10329
+ let currentVisibleStatus = "";
10330
+ let currentLoadingMessages;
10331
+ let lastStatusAt = 0;
10332
+ let pendingStatus = null;
10333
+ let pendingKey = "";
10334
+ let pendingTimer = null;
10335
+ let rotationTimer = null;
10336
+ let inflightStatusUpdate = Promise.resolve();
10337
+ const enqueueStatusUpdate = (task) => {
10338
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
10339
+ await task();
10340
+ });
10341
+ inflightStatusUpdate = request.catch(() => void 0);
10342
+ return request;
10343
+ };
10344
+ const scheduleRotation = () => {
10345
+ if (rotationTimer) {
10346
+ clearTimer(rotationTimer);
10347
+ rotationTimer = null;
10348
+ }
10349
+ if (!active || !currentVisibleStatus) {
10350
+ return;
10351
+ }
10352
+ rotationTimer = setTimer(() => {
10353
+ rotationTimer = null;
10354
+ if (!active || !currentVisibleStatus) {
10355
+ return;
10356
+ }
10357
+ void postStatus(currentVisibleStatus, currentLoadingMessages);
10358
+ }, STATUS_ROTATION_INTERVAL_MS);
10359
+ };
10360
+ const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
10361
+ const getInitialStatusText = () => {
10362
+ if (loadingMessages?.length) {
10363
+ return loadingMessages[0];
10364
+ }
10365
+ return defaultStatus.text;
10366
+ };
10367
+ const haveSameLoadingMessages = (left, right) => {
10368
+ if (left === right) {
10369
+ return true;
10370
+ }
10371
+ if (!left || !right || left.length !== right.length) {
10372
+ return false;
10373
+ }
10374
+ return left.every((message, index) => message === right[index]);
10375
+ };
10376
+ const postStatus = async (text, nextLoadingMessages) => {
10377
+ if (!text && !currentVisibleStatus) {
10378
+ return;
10379
+ }
10380
+ currentVisibleStatus = text;
10381
+ currentLoadingMessages = nextLoadingMessages;
10382
+ lastStatusAt = now();
10383
+ scheduleRotation();
10384
+ await enqueueStatusUpdate(async () => {
10385
+ await args.sendStatus(text, nextLoadingMessages);
10386
+ });
10387
+ };
10388
+ const postRenderedStatus = async (status) => {
10389
+ const presentation = renderAssistantStatus({
10390
+ status
10391
+ });
10392
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
10393
+ presentation.visible
10394
+ );
10395
+ currentKey = presentation.key;
10396
+ await postStatus(presentation.visible, nextLoadingMessages);
10397
+ };
10398
+ const clearPending = () => {
10399
+ if (pendingTimer) {
10400
+ clearTimer(pendingTimer);
10401
+ pendingTimer = null;
10402
+ }
10403
+ pendingStatus = null;
10404
+ pendingKey = "";
10405
+ };
10406
+ const flushPending = async () => {
10407
+ if (!active || !pendingStatus) {
10408
+ clearPending();
10409
+ return;
10410
+ }
10411
+ const next = pendingStatus;
10412
+ clearPending();
10413
+ const nextPresentation = renderAssistantStatus({
10414
+ status: next
10415
+ });
10416
+ if (nextPresentation.key !== currentKey) {
10417
+ await postRenderedStatus(next);
10418
+ }
10419
+ };
10420
+ return {
10421
+ start() {
10422
+ active = true;
10423
+ clearPending();
10424
+ currentKey = "initial";
10425
+ void postStatus(getInitialStatusText(), loadingMessages);
10426
+ },
10427
+ async stop() {
10428
+ active = false;
10429
+ clearPending();
10430
+ if (rotationTimer) {
10431
+ clearTimer(rotationTimer);
10432
+ rotationTimer = null;
10433
+ }
10434
+ currentKey = "";
10435
+ await postStatus("");
10436
+ },
10437
+ update(status) {
10438
+ if (!active) {
10439
+ return;
10440
+ }
10441
+ const presentation = renderAssistantStatus({
10442
+ status
10443
+ });
10444
+ if (!presentation.visible) {
10445
+ return;
10446
+ }
10447
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
10448
+ return;
10449
+ }
10450
+ if (presentation.visible === currentVisibleStatus) {
10451
+ clearPending();
10452
+ currentKey = presentation.key;
10453
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
10454
+ presentation.visible
10455
+ );
10456
+ if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) {
10457
+ void postStatus(presentation.visible, nextLoadingMessages);
10458
+ }
10459
+ return;
10460
+ }
10461
+ const elapsed = now() - lastStatusAt;
10462
+ const waitMs = Math.max(
10463
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
10464
+ STATUS_MIN_VISIBLE_MS - elapsed,
10465
+ 0
10466
+ );
10467
+ if (waitMs <= 0) {
10468
+ clearPending();
10469
+ void postRenderedStatus(status);
10470
+ return;
10471
+ }
10472
+ pendingStatus = status;
10473
+ pendingKey = presentation.key;
10474
+ if (pendingTimer) {
10475
+ return;
10476
+ }
10477
+ pendingTimer = setTimer(
10478
+ () => {
10479
+ pendingTimer = null;
10480
+ void flushPending();
10481
+ },
10482
+ Math.max(1, waitMs)
10483
+ );
10484
+ }
10485
+ };
10486
+ }
10487
+
10488
+ // src/chat/slack/assistant-thread/status-send.ts
10489
+ var SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
10490
+ function createSlackAdapterStatusSender(args) {
10491
+ const adapter = args.getSlackAdapter();
10492
+ const boundToken = getSlackAdapterRequestToken(adapter);
10493
+ return async (text, loadingMessages) => {
10494
+ const channelId = args.channelId;
10495
+ const threadTs = args.threadTs;
10496
+ if (!channelId || !threadTs) {
10497
+ return;
10498
+ }
10499
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
10500
+ if (!normalizedChannelId) {
10501
+ return;
10502
+ }
10503
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
10504
+ try {
10505
+ await runWithBoundSlackToken(
10506
+ adapter,
10507
+ boundToken,
10508
+ () => adapter.setAssistantStatus(
10509
+ normalizedChannelId,
10510
+ threadTs,
10511
+ text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
10512
+ nextLoadingMessages
10513
+ )
10514
+ );
10515
+ } catch (error) {
10516
+ logAssistantStatusFailure({
10517
+ status: text,
10518
+ error,
10519
+ channelId,
10520
+ normalizedChannelId,
10521
+ threadTs
10522
+ });
10523
+ }
10524
+ };
10525
+ }
10526
+ function createSlackWebApiStatusSender(args) {
10527
+ const getClient2 = args.getSlackClient ?? getSlackClient;
10528
+ return async (text, loadingMessages) => {
10529
+ const channelId = args.channelId;
10530
+ const threadTs = args.threadTs;
10531
+ if (!channelId || !threadTs) {
10532
+ return;
10533
+ }
10534
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
10535
+ if (!normalizedChannelId) {
10536
+ return;
10537
+ }
10538
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
10539
+ try {
10540
+ await getClient2().assistant.threads.setStatus({
10541
+ channel_id: normalizedChannelId,
10542
+ thread_ts: threadTs,
10543
+ status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
10544
+ ...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : {}
10545
+ });
10546
+ } catch (error) {
10547
+ logAssistantStatusFailure({
10548
+ status: text,
10549
+ error,
10550
+ channelId,
10551
+ normalizedChannelId,
10552
+ threadTs
10553
+ });
10554
+ }
10555
+ };
10556
+ }
10557
+ function getSlackAdapterRequestToken(adapter) {
10558
+ const token = adapter.requestContext?.getStore()?.token;
10559
+ if (typeof token !== "string") {
10560
+ return void 0;
10561
+ }
10562
+ const trimmed = token.trim();
10563
+ return trimmed || void 0;
10564
+ }
10565
+ async function runWithBoundSlackToken(adapter, token, task) {
10566
+ if (!token) {
10567
+ return await task();
10568
+ }
10569
+ return await adapter.withBotToken(token, task);
10570
+ }
10571
+ function logAssistantStatusFailure(args) {
10572
+ logWarn(
10573
+ "assistant_status_update_failed",
10574
+ {},
10575
+ {
10576
+ "app.slack.status_text": args.status || "(clear)",
10577
+ "app.slack.channel_id_raw": args.channelId,
10578
+ "app.slack.channel_id": args.normalizedChannelId,
10579
+ "app.slack.thread_ts": args.threadTs,
10580
+ "error.message": args.error instanceof Error ? args.error.message : String(args.error)
10581
+ },
10582
+ `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
10583
+ );
10584
+ }
10585
+
10586
+ // src/chat/slack/assistant-thread/status.ts
10587
+ function createSlackAdapterAssistantStatusSession(args) {
10588
+ return createAssistantStatusScheduler({
10589
+ sendStatus: createSlackAdapterStatusSender({
10590
+ channelId: args.channelId,
10591
+ threadTs: args.threadTs,
10592
+ getSlackAdapter: args.getSlackAdapter
10593
+ }),
10594
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
10595
+ now: args.now,
10596
+ setTimer: args.setTimer,
10597
+ clearTimer: args.clearTimer,
10598
+ random: args.random
10599
+ });
10600
+ }
10601
+ function createSlackWebApiAssistantStatusSession(args) {
10602
+ return createAssistantStatusScheduler({
10603
+ sendStatus: createSlackWebApiStatusSender({
10604
+ channelId: args.channelId,
10605
+ threadTs: args.threadTs,
10606
+ getSlackClient: args.getSlackClient
10607
+ }),
10608
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
10609
+ now: args.now,
10610
+ setTimer: args.setTimer,
10611
+ clearTimer: args.clearTimer,
10612
+ random: args.random
10613
+ });
10614
+ }
10615
+
10682
10616
  // src/chat/slack/footer.ts
10683
10617
  function escapeSlackMrkdwn(text) {
10684
10618
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");