@geminixiang/mikan 1.0.0-beta.40 → 1.0.0-beta.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/adapters/discord/bot.d.ts.map +1 -1
  3. package/dist/adapters/discord/bot.js +7 -6
  4. package/dist/adapters/discord/bot.js.map +1 -1
  5. package/dist/adapters/discord/components.d.ts +33 -0
  6. package/dist/adapters/discord/components.d.ts.map +1 -0
  7. package/dist/adapters/discord/components.js +32 -0
  8. package/dist/adapters/discord/components.js.map +1 -0
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +16 -6
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/discord/format.d.ts +3 -0
  13. package/dist/adapters/discord/format.d.ts.map +1 -0
  14. package/dist/adapters/discord/format.js +86 -0
  15. package/dist/adapters/discord/format.js.map +1 -0
  16. package/dist/adapters/markdown-tables.d.ts +37 -0
  17. package/dist/adapters/markdown-tables.d.ts.map +1 -0
  18. package/dist/adapters/markdown-tables.js +164 -0
  19. package/dist/adapters/markdown-tables.js.map +1 -0
  20. package/dist/adapters/progressive-renderer.d.ts.map +1 -1
  21. package/dist/adapters/progressive-renderer.js +69 -12
  22. package/dist/adapters/progressive-renderer.js.map +1 -1
  23. package/dist/adapters/slack/assistant.d.ts +101 -0
  24. package/dist/adapters/slack/assistant.d.ts.map +1 -0
  25. package/dist/adapters/slack/assistant.js +204 -0
  26. package/dist/adapters/slack/assistant.js.map +1 -0
  27. package/dist/adapters/slack/blocks.d.ts.map +1 -1
  28. package/dist/adapters/slack/blocks.js +4 -82
  29. package/dist/adapters/slack/blocks.js.map +1 -1
  30. package/dist/adapters/slack/bot.d.ts +52 -0
  31. package/dist/adapters/slack/bot.d.ts.map +1 -1
  32. package/dist/adapters/slack/bot.js +161 -5
  33. package/dist/adapters/slack/bot.js.map +1 -1
  34. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  35. package/dist/adapters/slack/response-lifecycle.js +86 -15
  36. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  37. package/dist/adapters/slack/stream-limits.d.ts +35 -0
  38. package/dist/adapters/slack/stream-limits.d.ts.map +1 -0
  39. package/dist/adapters/slack/stream-limits.js +56 -0
  40. package/dist/adapters/slack/stream-limits.js.map +1 -0
  41. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  42. package/dist/adapters/telegram/bot.js +26 -47
  43. package/dist/adapters/telegram/bot.js.map +1 -1
  44. package/dist/adapters/telegram/context.d.ts.map +1 -1
  45. package/dist/adapters/telegram/context.js +7 -19
  46. package/dist/adapters/telegram/context.js.map +1 -1
  47. package/dist/adapters/types.d.ts +24 -1
  48. package/dist/adapters/types.d.ts.map +1 -1
  49. package/dist/adapters/types.js.map +1 -1
  50. package/dist/agent.d.ts.map +1 -1
  51. package/dist/agent.js +2 -1
  52. package/dist/agent.js.map +1 -1
  53. package/dist/commands/extensions.d.ts +18 -6
  54. package/dist/commands/extensions.d.ts.map +1 -1
  55. package/dist/commands/extensions.js +55 -8
  56. package/dist/commands/extensions.js.map +1 -1
  57. package/dist/env-manifest.d.ts.map +1 -1
  58. package/dist/env-manifest.js +4 -0
  59. package/dist/env-manifest.js.map +1 -1
  60. package/dist/harness/subagent-runner.d.ts +6 -0
  61. package/dist/harness/subagent-runner.d.ts.map +1 -1
  62. package/dist/harness/subagent-runner.js +63 -0
  63. package/dist/harness/subagent-runner.js.map +1 -1
  64. package/dist/subagent-progress.d.ts +3 -7
  65. package/dist/subagent-progress.d.ts.map +1 -1
  66. package/dist/subagent-progress.js +15 -10
  67. package/dist/subagent-progress.js.map +1 -1
  68. package/dist/tools/subagent.d.ts +8 -1
  69. package/dist/tools/subagent.d.ts.map +1 -1
  70. package/dist/tools/subagent.js +24 -1
  71. package/dist/tools/subagent.js.map +1 -1
  72. package/dist/types.d.ts +8 -0
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/web/admin/portal.js +5 -1
  76. package/dist/web/admin/portal.js.map +1 -1
  77. package/package.json +2 -1
  78. package/dist/adapters/telegram/html.d.ts +0 -3
  79. package/dist/adapters/telegram/html.d.ts.map +0 -1
  80. package/dist/adapters/telegram/html.js +0 -101
  81. package/dist/adapters/telegram/html.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { SocketModeClient } from "@slack/socket-mode";
2
2
  import { WebAPIRateLimitedError, WebClient } from "@slack/web-api";
3
+ import { readEnv } from "../../env-manifest.js";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { readFile, writeFile } from "node:fs/promises";
5
6
  import { basename, join } from "node:path";
@@ -13,13 +14,31 @@ import { isRecord, readTextFileIfExists } from "../../utils/file-guards.js";
13
14
  import { PRODUCT_NAME, formatForceStopped } from "../../platform-messages.js";
14
15
  import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, saveIncomingAttachments, withRetry, } from "../shared.js";
15
16
  import { processMessageIntake } from "../intake.js";
17
+ import { AssistantThreadRegistry, handleAgentContextChanged, handleAgentDmOpened, handleAssistantThreadStarted, titleAssistantThread, } from "./assistant.js";
16
18
  import { createSlackAdapters } from "./context.js";
17
19
  import { hasMaterializedChatSession, registerThreadSession, } from "../../sessions/chat-history-sync.js";
18
20
  import { conversationIdOf } from "../../sessions/session-key.js";
19
21
  import { isSlackThreadSessionKey, planSlackAdapterSession, planSlackEventAnchorRun, resolveSlackSessionKey, } from "./session.js";
20
22
  import { reportUserFacingError } from "../../observability/sentry.js";
21
23
  import { renderSlackBlocks, resolveSlackMentions } from "./blocks.js";
24
+ import { StreamStartLimiter } from "./stream-limits.js";
22
25
  const SLACK_EVENT_ANCHOR_TEXT = "Working on it...";
26
+ /**
27
+ * Slack's per-call limit on `markdown_text`: "Limit this field to 12,000
28
+ * characters." mikan allows far more in a message, so streamed text is split
29
+ * to fit rather than truncated.
30
+ */
31
+ const MAX_STREAM_TEXT_CHARS = 12_000;
32
+ /** Slices `text` into pieces that each fit one streaming call. */
33
+ export function chunkStreamText(text, limit = MAX_STREAM_TEXT_CHARS) {
34
+ if (text.length <= limit)
35
+ return [text];
36
+ const chunks = [];
37
+ for (let index = 0; index < text.length; index += limit) {
38
+ chunks.push(text.slice(index, index + limit));
39
+ }
40
+ return chunks;
41
+ }
23
42
  // web-api v8 throws WebAPIRateLimitedError; the duck-typed shapes cover
24
43
  // platform `data.error === "rate_limited"` / 429 responses and older callers.
25
44
  function slackIsRateLimited(err) {
@@ -112,6 +131,15 @@ export class SlackMessagingBot {
112
131
  this.startupTs = null; // Messages older than this are just logged, not processed
113
132
  this.users = new Map();
114
133
  this.channels = new Map();
134
+ /** Which threads belong to the assistant pane, and their last known context. */
135
+ this.assistantThreads = new AssistantThreadRegistry();
136
+ /**
137
+ * Native response streams opened recently. Per bot rather than per module:
138
+ * the rate tier applies per workspace, and this bot is the connection to
139
+ * one — which also means a test constructing a bot gets a fresh budget
140
+ * instead of inheriting whatever ran before it.
141
+ */
142
+ this.streamStarts = new StreamStartLimiter();
115
143
  this.queues = new Map();
116
144
  this.eventsWatcher = null;
117
145
  this.handler = handler;
@@ -319,22 +347,54 @@ export class SlackMessagingBot {
319
347
  await this.webClient.chat.update(payload);
320
348
  });
321
349
  }
350
+ /**
351
+ * Claim one native stream, or decline. Declining is not an error: the caller
352
+ * falls back to edit-based updates, which is a working answer rather than a
353
+ * degraded one, and costs no request against the tighter start tier.
354
+ */
355
+ tryReserveStreamStart() {
356
+ return this.streamStarts.tryReserve();
357
+ }
358
+ /**
359
+ * Open a stream, carrying text of any length.
360
+ *
361
+ * Slack caps `markdown_text` at 12,000 characters per call, and a long first
362
+ * render exceeds that easily — mikan allows 35,000 in a message. Rather than
363
+ * truncating (losing the answer) or failing (falling back to edit mode for
364
+ * the whole response), the opening call takes the first slice and the rest
365
+ * follows as appends before this resolves. The caller sees one operation
366
+ * that sent everything, so the renderer's idea of what has been streamed
367
+ * stays true.
368
+ *
369
+ * Splitting mid-word or mid-fence is harmless here: a stream concatenates,
370
+ * so Slack renders the reassembled text, not the pieces.
371
+ */
322
372
  async startMessageStream(channel, text, threadTs, recipientUserId) {
323
- return slackRetry(async () => {
373
+ const [head, ...tail] = chunkStreamText(text);
374
+ const ts = await slackRetry(async () => {
324
375
  const result = await this.webClient.apiCall("chat.startStream", {
325
376
  channel,
326
- markdown_text: this.resolveMentions(text),
377
+ markdown_text: this.resolveMentions(head ?? ""),
327
378
  ...(threadTs ? { thread_ts: threadTs } : {}),
328
379
  ...(this.teamId ? { recipient_team_id: this.teamId } : {}),
329
380
  ...(recipientUserId ? { recipient_user_id: recipientUserId } : {}),
330
381
  });
331
- const ts = result.ts;
332
- if (!ts)
382
+ const streamTs = result.ts;
383
+ if (!streamTs)
333
384
  throw new Error("Slack chat.startStream did not return ts");
334
- return ts;
385
+ return streamTs;
335
386
  });
387
+ for (const part of tail)
388
+ await this.appendStreamChunk(channel, ts, part);
389
+ return ts;
336
390
  }
391
+ /** Append a delta of any length, split to the per-call cap. */
337
392
  async appendMessageStream(channel, ts, text) {
393
+ for (const part of chunkStreamText(text)) {
394
+ await this.appendStreamChunk(channel, ts, part);
395
+ }
396
+ }
397
+ async appendStreamChunk(channel, ts, text) {
338
398
  return slackRetry(async () => {
339
399
  await this.webClient.apiCall("chat.appendStream", {
340
400
  channel,
@@ -368,6 +428,69 @@ export class SlackMessagingBot {
368
428
  });
369
429
  });
370
430
  }
431
+ /** Offer starting points in a new assistant thread. */
432
+ async setAssistantSuggestedPrompts(channel, threadTs, prompts) {
433
+ return slackRetry(async () => {
434
+ // Omitting thread_ts pins the prompts to the DM itself, which is what
435
+ // the agent surface wants: there is no thread yet when it opens.
436
+ await this.webClient.assistant.threads.setSuggestedPrompts({
437
+ channel_id: channel,
438
+ ...(threadTs ? { thread_ts: threadTs } : {}),
439
+ prompts,
440
+ });
441
+ });
442
+ }
443
+ /** Name an assistant thread, which is what the sidebar conversation list shows. */
444
+ async setAssistantTitle(channel, threadTs, title) {
445
+ return slackRetry(async () => {
446
+ await this.webClient.assistant.threads.setTitle({
447
+ channel_id: channel,
448
+ thread_ts: threadTs,
449
+ title,
450
+ });
451
+ });
452
+ }
453
+ /** The assistant surface's view of this bot, handed to the lifecycle handlers. */
454
+ assistantOps() {
455
+ return {
456
+ postInThread: (channel, threadTs, text) => this.postInThread(channel, threadTs, text),
457
+ setSuggestedPrompts: (channel, threadTs, prompts) => this.setAssistantSuggestedPrompts(channel, threadTs, prompts),
458
+ setTitle: (channel, threadTs, title) => this.setAssistantTitle(channel, threadTs, title),
459
+ channelName: (channelId) => this.channels.get(channelId)?.name,
460
+ };
461
+ }
462
+ /**
463
+ * A new conversation opened in the assistant pane. Slack waits for the app
464
+ * to speak first here; staying silent is what makes the pane look broken.
465
+ */
466
+ handleAssistantThreadStarted({ event, ack }) {
467
+ ack();
468
+ const payload = event;
469
+ if (!payload.assistant_thread)
470
+ return;
471
+ void handleAssistantThreadStarted(this.assistantOps(), this.assistantThreads, payload.assistant_thread);
472
+ }
473
+ /**
474
+ * The person navigated to another channel while the pane is open. Slack
475
+ * sends this as `assistant_thread_context_changed` under `assistant_view`
476
+ * and `app_context_changed` under `agent_view`; the payload differs enough
477
+ * to normalize here rather than in the handler.
478
+ */
479
+ handleAgentContextChangedEvent({ event, ack, }) {
480
+ ack();
481
+ const payload = event;
482
+ if (payload.assistant_thread) {
483
+ handleAgentContextChanged(this.assistantThreads, payload.assistant_thread);
484
+ return;
485
+ }
486
+ const channelId = payload.channel_id ?? payload.channel;
487
+ if (channelId) {
488
+ handleAgentContextChanged(this.assistantThreads, {
489
+ channel_id: channelId,
490
+ context: payload.context,
491
+ });
492
+ }
493
+ }
371
494
  async postInThread(channel, threadTs, text) {
372
495
  return slackRetry(async () => {
373
496
  const payload = {
@@ -885,10 +1008,27 @@ export class SlackMessagingBot {
885
1008
  this.socketClient.on("unable_to_socket_mode_start", (err) => {
886
1009
  log.logWarning("Slack socket unable_to_start", err ? String(err) : "");
887
1010
  });
1011
+ // Which events Slack actually delivers is otherwise unknowable from here:
1012
+ // a listener that never fires looks the same whether the subscription is
1013
+ // missing, the app needs reinstalling, or the surface never emits it at
1014
+ // all. Off by default — this is one line per event on a busy workspace.
1015
+ if (readEnv("SLACK_TRACE_EVENTS")) {
1016
+ this.socketClient.on("slack_event", ({ type, body }) => {
1017
+ const inner = body?.event;
1018
+ log.logInfo(`Slack socket event: ${type ?? "?"}${inner?.type ? ` → ${inner.type}` : ""}${inner?.tab ? ` (tab: ${inner.tab})` : ""}`);
1019
+ });
1020
+ }
888
1021
  this.socketClient.on("app_mention", (payload) => this.handleAppMention(payload));
889
1022
  this.socketClient.on("message", (payload) => this.handleMessageEvent(payload));
890
1023
  this.socketClient.on("slash_commands", (payload) => void this.handleSlashCommand(payload));
891
1024
  this.socketClient.on("app_home_opened", (payload) => this.handleAppHomeOpened(payload));
1025
+ // The assistant surface's lifecycle. Both events were already subscribed
1026
+ // in the app manifest and then dropped on the floor, which is why the pane
1027
+ // opened empty and the sidebar filled with untitled conversations.
1028
+ this.socketClient.on("assistant_thread_started", (payload) => this.handleAssistantThreadStarted(payload));
1029
+ this.socketClient.on("assistant_thread_context_changed", (payload) => this.handleAgentContextChangedEvent(payload));
1030
+ // agent_view's spelling of the same signal.
1031
+ this.socketClient.on("app_context_changed", (payload) => this.handleAgentContextChangedEvent(payload));
892
1032
  this.socketClient.on("block_actions", (payload) => void this.handleBlockAction(payload));
893
1033
  this.socketClient.on("interactive", (payload) => void this.handleBlockAction(payload));
894
1034
  }
@@ -992,6 +1132,12 @@ export class SlackMessagingBot {
992
1132
  }
993
1133
  const isThreadReply = !!e.thread_ts;
994
1134
  const sessionKey = isDM ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
1135
+ // Name the conversation from the first thing the person said. Guarded on
1136
+ // the assistant registry, so classic DM threads are untouched, and it
1137
+ // claims the title once per thread.
1138
+ if (isDM && e.thread_ts && e.text) {
1139
+ void titleAssistantThread(this.assistantOps(), this.assistantThreads, e.channel, e.thread_ts, this.stripOwnMention(e.text));
1140
+ }
995
1141
  const slackEvent = createConversationEvent({
996
1142
  platform: "slack",
997
1143
  conversationId: e.channel,
@@ -1072,6 +1218,16 @@ export class SlackMessagingBot {
1072
1218
  handleAppHomeOpened({ event, ack }) {
1073
1219
  const e = event;
1074
1220
  ack();
1221
+ // Under agent_view this is how Slack says "the person opened the app's
1222
+ // DM" — the signal assistant_thread_started used to carry. It fires on
1223
+ // every open, not once per conversation, so this path refreshes the
1224
+ // pinned prompts and deliberately does not greet.
1225
+ if (e.tab === "messages") {
1226
+ if (e.channel) {
1227
+ void handleAgentDmOpened(this.assistantOps(), this.assistantThreads, e.channel, e.context);
1228
+ }
1229
+ return;
1230
+ }
1075
1231
  if (e.tab !== "home")
1076
1232
  return;
1077
1233
  this.webClient.views