@sentry/junior 0.267.0 → 0.268.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
  2. package/dist/{adapter-BybMEezj.js → adapter-04AcD2SO.js} +4 -6
  3. package/dist/{admin-DOs1_EiW.js → admin-DIU1QyG3.js} +1 -1
  4. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  5. package/dist/{agent-runner-DgNATN4D.js → agent-runner-CAYfFmU7.js} +4409 -3020
  6. package/dist/api/schema.d.ts +2 -5629
  7. package/dist/api/schema.js +4 -4
  8. package/dist/api.d.ts +1 -1
  9. package/dist/api.js +154 -32
  10. package/dist/{app-DAB3fzyp.d.ts → app-z6Uuyq3j.d.ts} +3 -2
  11. package/dist/app.d.ts +1 -1
  12. package/dist/app.js +332 -98
  13. package/dist/{briefs-CWKjka7v.js → briefs-DQbV2jNr.js} +2 -2
  14. package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BD9GSpc6.js} +1 -1
  15. package/dist/cli/chat.js +15 -15
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/main.js +3 -3
  18. package/dist/cli/plugins.js +4 -4
  19. package/dist/cli/snapshot-create.js +4 -4
  20. package/dist/cli/upgrade.d.ts +1 -1
  21. package/dist/cli/upgrade.js +3 -3
  22. package/dist/{client-BjgbMawj.js → client-C1yDYjoS.js} +2 -2
  23. package/dist/{config-CdB6yJj5.js → config-DrTeF80T.js} +2 -2
  24. package/dist/{connections-RE_65jzK.js → connections-D46f8vvi.js} +10 -11
  25. package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-CxlOCiUo.js} +1 -1
  26. package/dist/{credential-sync-DhEGEpgq.js → credential-sync-sm9fegLY.js} +29 -34
  27. package/dist/{credentialed-CIGwoLU2.js → credentialed-lukuD-cc.js} +39 -13
  28. package/dist/db-BU4LQD8s.d.ts +6 -0
  29. package/dist/{executor-VYrx_K4w.js → executor-4vhvsMZJ.js} +78 -4
  30. package/dist/instrumentation.js +2 -2
  31. package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-CkncDT8d.js} +8 -2
  32. package/dist/{job-queue-C0BZSa7r.js → job-queue-In9QKz-I.js} +2 -2
  33. package/dist/{list-1XQP-o6K.js → list-BkgnXEEB.js} +298 -807
  34. package/dist/{logging-DuZ1A4ab.js → logging-DLSpkSSr.js} +1 -1
  35. package/dist/{message-NKSbbnA6.js → message-BObyg59s.js} +4 -4
  36. package/dist/nitro.d.ts +1 -1
  37. package/dist/nitro.js +3 -3
  38. package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-B-WbSdoH.js} +2 -2
  39. package/dist/{registry-FQutO926.js → registry-BgecKva0.js} +2 -2
  40. package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
  41. package/dist/{resolve-Bk4yeuHy.js → resolve-Cpa4Bu1O.js} +3 -3
  42. package/dist/{result-B9e9ATOU.js → result-D3TSmZZ8.js} +8 -8
  43. package/dist/{runner-C3S2ObH-.js → runner-DS5OKhEp.js} +10 -10
  44. package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
  45. package/dist/{schema-X7Fj4fv1.js → schema-0050HFnB.js} +80 -4
  46. package/dist/schema-ZngqcOWM.d.ts +5813 -0
  47. package/dist/{service-account-Do30HUNv.js → service-account-CKV2WTY7.js} +3 -3
  48. package/dist/{service-account-BQasl-qq.js → service-account-DGjnDw9y.js} +3 -3
  49. package/dist/{session-stop-DkOjt46_.js → session-stop-Dam1EI7B.js} +494 -18
  50. package/dist/{skills-CQHrwfOa.js → skills-CfAnfImD.js} +2 -2
  51. package/dist/{spawn-BHuqN14f.js → spawn-cmq9p4Ly.js} +2 -2
  52. package/dist/{store-6EetVP-m.js → store-C5PN02QO.js} +4 -4
  53. package/dist/{store-B8CABbmR.d.ts → store-xMuuXRE0.d.ts} +13 -7
  54. package/dist/{store-DvNZIsCr.js → store-zPEoJPBB.js} +3 -3
  55. package/dist/{task-queue-B0DLgd2P.js → task-queue-_mf28dSu.js} +3 -3
  56. package/dist/{task-runner-B-PD_JOj.js → task-runner-DMI2prDO.js} +11 -12
  57. package/dist/{tasks-0ilmuCux.js → tasks-SB9z-VTU.js} +124 -21
  58. package/dist/{thread-state-DRmdJ2QN.js → thread-state-BbbPh0Sn.js} +8 -18
  59. package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-BKKmnMB5.js} +10 -15
  60. package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-oP0kVeGz.js} +1 -1
  61. package/dist/{validation-CxEmt1id.js → validation--JsAGLwc.js} +1 -1
  62. package/dist/{version-Bapedn4A.js → version-jHwJp4hC.js} +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/{viewer-DrN3oC2X.js → viewer-CpXTGlep.js} +323 -235
  65. package/dist/{user-input-request-BDFBdF8r.js → web-message-id-DlHNANtd.js} +17 -1
  66. package/dist/{work-DUQoNcqP.js → work-3N-GHY9m.js} +10 -10
  67. package/dist/{worker-D1l4Ugk0.js → worker-CCdbcItd.js} +6 -6
  68. package/migrations/0065_user_customization.sql +1 -0
  69. package/migrations/0066_review_items.sql +53 -0
  70. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  71. package/migrations/0068_review_item_slack_cards.sql +10 -0
  72. package/migrations/meta/0065_snapshot.json +3511 -0
  73. package/migrations/meta/0066_snapshot.json +4099 -0
  74. package/migrations/meta/0067_snapshot.json +3921 -0
  75. package/migrations/meta/0068_snapshot.json +4176 -0
  76. package/migrations/meta/_journal.json +29 -1
  77. package/package.json +5 -5
  78. package/dist/db-Bg-zC3r5.d.ts +0 -5
  79. package/dist/images-CecjXAAz.js +0 -64
  80. package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
@@ -1,6 +1,6 @@
1
- import { B as isRecord, b as setSpanAttributes, h as logWarn } from "./logging-DuZ1A4ab.js";
2
- import { c as fetchWithBoundedOAuthErrorBodies, i as resolvePluginHeaderEnvRefs } from "./registry-FQutO926.js";
3
- import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, s as fenceLock } from "./adapter-BybMEezj.js";
1
+ import { B as isRecord, b as setSpanAttributes, h as logWarn } from "./logging-DLSpkSSr.js";
2
+ import { c as fetchWithBoundedOAuthErrorBodies, i as resolvePluginHeaderEnvRefs } from "./registry-BgecKva0.js";
3
+ import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, s as fenceLock } from "./adapter-04AcD2SO.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { UnauthorizedError, auth } from "@modelcontextprotocol/sdk/client/auth.js";
@@ -1,6 +1,6 @@
1
- import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
2
- import { t as disconnectStateAdapter } from "./adapter-BybMEezj.js";
3
- import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-Do30HUNv.js";
1
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-BD9GSpc6.js";
2
+ import { t as disconnectStateAdapter } from "./adapter-04AcD2SO.js";
3
+ import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-CKV2WTY7.js";
4
4
  import { CLI_USAGE } from "./cli/run.js";
5
5
  import { createServer } from "node:http";
6
6
  // src/cli/service-account.ts
@@ -1,19 +1,19 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { A as CORE_EVENT_NAMESPACE, Gn as normalizeToolNameFromResult, N as eventCatalogEntry, P as eventGuidance, Un as isToolResultError, Vn as isAssistantMessage, Wn as isToolResultMessage } from "./tasks-0ilmuCux.js";
3
- import { n as botConfig } from "./config-CdB6yJj5.js";
4
- import { h as logWarn, m as logInfo, p as logException } from "./logging-DuZ1A4ab.js";
5
- import { f as getConversationStore, m as getSqlExecutor } from "./viewer-DrN3oC2X.js";
6
- import { s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
7
- import "./executor-VYrx_K4w.js";
8
- import { h as credentialSubjectSchema } from "./registry-FQutO926.js";
9
- import { $ as postSlackMessage, Zt as verifyTimerWatchCredentialSubject, dn as JUNIOR_THREAD_STATE_TTL_MS, gn as ToolInputError, p as getPlugins } from "./credentialed-CIGwoLU2.js";
10
- import { i as getStateAdapter } from "./adapter-BybMEezj.js";
2
+ import { F as eventCatalogEntry, Gn as isToolResultError, I as eventGuidance, Kn as isToolResultMessage, M as CORE_EVENT_NAMESPACE, Un as isAssistantMessage, qn as normalizeToolNameFromResult } from "./tasks-SB9z-VTU.js";
3
+ import { n as botConfig } from "./config-DrTeF80T.js";
4
+ import { h as logWarn, m as logInfo, p as logException } from "./logging-DLSpkSSr.js";
5
+ import { h as getSqlExecutor, p as getConversationStore } from "./viewer-CpXTGlep.js";
6
+ import { s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
7
+ import "./executor-4vhvsMZJ.js";
8
+ import { h as credentialSubjectSchema } from "./registry-BgecKva0.js";
9
+ import { $t as verifyTimerWatchCredentialSubject, G as normalizeSlackReplyMarkdown, U as escapeSlackMrkdwnText, W as formatSlackLink, Yn as buildSentryConversationUrl, hn as getDashboardObjectIconLink, l as getPluginSlackConversationLink, mn as getDashboardConversationLink, p as getPlugins, pn as JUNIOR_THREAD_STATE_TTL_MS, tt as postSlackMessage, yn as ToolInputError } from "./credentialed-lukuD-cc.js";
10
+ import { i as getStateAdapter } from "./adapter-04AcD2SO.js";
11
11
  import { r as EVENT_AUTHOR_ID } from "./prompt-BW9jFsqZ.js";
12
- import { r as sanitizeAssistantText } from "./assistant-reply-CT7VcXQe.js";
13
- import { r as appendAndEnqueueInboundMessage } from "./store-DvNZIsCr.js";
12
+ import { i as sanitizeAssistantText, t as getInterruptionMarker } from "./interruption-marker-CkncDT8d.js";
13
+ import { r as appendAndEnqueueInboundMessage } from "./store-zPEoJPBB.js";
14
14
  import { z } from "zod";
15
15
  import { eq, inArray } from "drizzle-orm";
16
- import { eventMatchFieldsSchema, objectTypeSchema } from "@sentry/junior-plugin-api";
16
+ import { eventMatchFieldsSchema, objectFactFields, objectPresentation, objectTypeSchema } from "@sentry/junior-plugin-api";
17
17
  // src/chat/automation-result.ts
18
18
  /**
19
19
  * Automation run result.
@@ -48,6 +48,17 @@ function deliversFinalText(dispatch) {
48
48
  return dispatch?.outcomes?.length !== 0 && !dispatch?.declaresResult;
49
49
  }
50
50
  /**
51
+ * Return whether the runtime may post a failure reply or a status notice,
52
+ * such as an auth prompt, for a run outside `finishedRunReply`. The rule is
53
+ * the same as for final text. A chat Turn may post, also in a thread that an
54
+ * Automation Conversation owns. A dispatch with no outcomes posts nothing. An
55
+ * Automation run posts only a declared message; its failed execution shows on
56
+ * the Automation.
57
+ */
58
+ function postsRunNotices(dispatch) {
59
+ return deliversFinalText(dispatch);
60
+ }
61
+ /**
51
62
  * Return the text to post after a run ends, if any. A chat Turn already
52
63
  * delivered a successful reply, so it posts only its failure reply. An
53
64
  * Automation run posts only a declared message. It never posts a failure
@@ -271,6 +282,467 @@ async function recordFinishedTurnForAutomatedLimit(args) {
271
282
  state: args.state
272
283
  });
273
284
  }
285
+ // src/chat/slack/automation-card.ts
286
+ function preview(text, length) {
287
+ const compact = text.replace(/\s+/g, " ").trim();
288
+ return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
289
+ }
290
+ /** Show an Automation as a native Work Object, not an operation receipt. */
291
+ function renderSlackAutomationCard(card) {
292
+ const title = preview(card.title, 160);
293
+ const iconUrl = getDashboardObjectIconLink("workflow");
294
+ const trigger = preview(card.trigger, 500);
295
+ const warning = card.warning ? preview(card.warning, 500) : null;
296
+ const text = [
297
+ "Automation",
298
+ card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
299
+ escapeSlackMrkdwnText(trigger),
300
+ warning ? escapeSlackMrkdwnText(warning) : null
301
+ ].filter(Boolean).join("\n");
302
+ if (!card.url) return {
303
+ entity: null,
304
+ text
305
+ };
306
+ return {
307
+ text,
308
+ entity: {
309
+ entity_type: "slack#/entities/item",
310
+ external_ref: {
311
+ id: card.id,
312
+ type: "automation"
313
+ },
314
+ url: card.url,
315
+ entity_payload: {
316
+ attributes: {
317
+ title: { text: title },
318
+ display_type: "Automation",
319
+ product_icon: iconUrl ? {
320
+ url: iconUrl,
321
+ alt_text: "Automation"
322
+ } : void 0
323
+ },
324
+ custom_fields: [{
325
+ key: "trigger",
326
+ label: "When",
327
+ type: "string",
328
+ value: trigger,
329
+ long: true
330
+ }, ...warning ? [{
331
+ key: "warning",
332
+ label: "Needs attention",
333
+ type: "string",
334
+ value: warning,
335
+ long: true
336
+ }] : []]
337
+ }
338
+ }
339
+ };
340
+ }
341
+ // src/chat/slack/object-card.ts
342
+ function descriptionPreview(value) {
343
+ const text = value?.replace(/\r\n/g, "\n").trim() ?? "";
344
+ const preview = text.split("\n").slice(0, 6).join("\n").slice(0, 500);
345
+ return preview.length < text.length ? `${preview.slice(0, 499).trimEnd()}…` : preview;
346
+ }
347
+ /** Render a compact Task or Item preview, or the saved facts for its detail panel. */
348
+ function renderSlackObjectCard(card, conversationId, surface = "preview") {
349
+ if (card.objectType === "automation" && card.plugin === "junior") return renderSlackAutomationCard({
350
+ id: card.key,
351
+ title: card.title,
352
+ url: card.url,
353
+ trigger: card.trigger ?? "",
354
+ warning: card.warning ?? null
355
+ });
356
+ const presentation = objectPresentation(card);
357
+ const type = card.displayType ?? presentation.label;
358
+ const iconUrl = getDashboardObjectIconLink(presentation.icon);
359
+ const title = card.title.slice(0, 160);
360
+ const details = surface === "details";
361
+ const deployment = card.objectType === "deployment" || card.objectType === "item" && card.facts?.type === "deployment";
362
+ const calendarEvent = card.objectType === "calendar_event";
363
+ const status = details || deployment || calendarEvent ? card.status : void 0;
364
+ const warning = details || deployment ? card.warning : void 0;
365
+ const facts = objectFactFields(card.facts).filter((field) => details || calendarEvent || deployment && field.key === "environment");
366
+ const description = details ? card.description?.trim() ?? "" : calendarEvent ? "" : descriptionPreview(card.description);
367
+ const text = [
368
+ escapeSlackMrkdwnText(type),
369
+ card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
370
+ escapeSlackMrkdwnText(card.label),
371
+ status ? escapeSlackMrkdwnText(status) : null,
372
+ description ? escapeSlackMrkdwnText(description) : null,
373
+ warning ? escapeSlackMrkdwnText(`Needs attention: ${warning}`) : null,
374
+ ...facts.map((field) => escapeSlackMrkdwnText(`${field.label}: ${field.value}`))
375
+ ].filter(Boolean).join("\n");
376
+ if (!card.url) return {
377
+ entity: null,
378
+ text
379
+ };
380
+ const task = card.objectType === "task";
381
+ const attributes = {
382
+ title: { text: title },
383
+ display_id: card.label,
384
+ display_type: type,
385
+ product_icon: iconUrl ? {
386
+ url: iconUrl,
387
+ alt_text: `${type}${status ? `: ${status}` : ""}`
388
+ } : void 0,
389
+ product_name: card.plugin
390
+ };
391
+ const customFields = [
392
+ ...description ? [{
393
+ key: "description",
394
+ label: "Description",
395
+ type: "string",
396
+ value: description,
397
+ long: true,
398
+ format: "markdown"
399
+ }] : [],
400
+ ...facts.map((field) => ({
401
+ ...field,
402
+ type: "string"
403
+ })),
404
+ ...warning ? [{
405
+ key: "warning",
406
+ label: "Needs attention",
407
+ type: "string",
408
+ value: warning
409
+ }] : []
410
+ ];
411
+ return {
412
+ text,
413
+ entity: {
414
+ ...task ? {
415
+ entity_type: "slack#/entities/task",
416
+ entity_payload: {
417
+ attributes,
418
+ display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
419
+ fields: status ? { status: { value: status } } : void 0,
420
+ custom_fields: customFields
421
+ }
422
+ } : {
423
+ entity_type: "slack#/entities/item",
424
+ entity_payload: {
425
+ attributes,
426
+ display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
427
+ custom_fields: [...status ? [{
428
+ key: "status",
429
+ label: "Status",
430
+ type: "string",
431
+ value: status
432
+ }] : [], ...customFields]
433
+ }
434
+ },
435
+ external_ref: {
436
+ id: Buffer.from(JSON.stringify([
437
+ conversationId,
438
+ card.plugin,
439
+ card.key
440
+ ]), "utf8").toString("base64url"),
441
+ type: "annotation"
442
+ },
443
+ url: card.url
444
+ }
445
+ };
446
+ }
447
+ // src/chat/slack/cards.ts
448
+ /** Select the Slack object renderer for a built-in card. */
449
+ function renderSlackCard(card, conversationId) {
450
+ switch (card.kind) {
451
+ case "object": return renderSlackObjectCard(card, conversationId);
452
+ case "automation": return renderSlackAutomationCard(card);
453
+ }
454
+ }
455
+ // src/chat/slack/footer.ts
456
+ /** Render compact reply attribution for the Slack footer. */
457
+ function formatReplyAttribution(attribution) {
458
+ return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
459
+ }
460
+ /**
461
+ * Build the compact conversation footer for visible Slack reply surfaces.
462
+ *
463
+ * Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
464
+ */
465
+ function buildSlackReplyFooter(args) {
466
+ const items = [];
467
+ const conversationId = args.conversationId?.trim();
468
+ if (conversationId) {
469
+ const idItem = {
470
+ label: "ID",
471
+ value: conversationId
472
+ };
473
+ const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
474
+ if (conversationUrl) idItem.url = conversationUrl;
475
+ items.push(idItem);
476
+ }
477
+ return items.length > 0 || args.replyAttribution ? {
478
+ ...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
479
+ items
480
+ } : void 0;
481
+ }
482
+ /** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
483
+ function buildSlackReplyBlocks(text, footer) {
484
+ const blocks = text.trim() ? [{
485
+ type: "markdown",
486
+ text
487
+ }] : [];
488
+ if (footer && (footer.attribution || footer.items.length > 0)) {
489
+ const attributionElements = footer.attribution ? [{
490
+ type: "plain_text",
491
+ text: formatReplyAttribution(footer.attribution)
492
+ }] : [];
493
+ blocks.push({
494
+ type: "context",
495
+ elements: [...attributionElements, ...footer.items.map((item) => ({
496
+ type: "mrkdwn",
497
+ text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
498
+ }))]
499
+ });
500
+ }
501
+ return blocks.length ? blocks : void 0;
502
+ }
503
+ // src/chat/slack/output.ts
504
+ const MAX_INLINE_CHARS = 2200;
505
+ const MAX_INLINE_LINES = 45;
506
+ const CONTINUED_MARKER = "\n\n[Continued below]";
507
+ function countSlackLines(text) {
508
+ if (!text) return 0;
509
+ return text.split("\n").length;
510
+ }
511
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
512
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
513
+ }
514
+ function findSplitIndex(text, maxChars) {
515
+ if (text.length <= maxChars) return text.length;
516
+ const bounded = text.slice(0, maxChars);
517
+ const newlineIndex = bounded.lastIndexOf("\n");
518
+ if (newlineIndex > 0) return newlineIndex;
519
+ const spaceIndex = bounded.lastIndexOf(" ");
520
+ if (spaceIndex > 0) return spaceIndex;
521
+ return maxChars;
522
+ }
523
+ function splitByLineBudget(text, maxLines) {
524
+ if (maxLines <= 0) return "";
525
+ const lines = text.split("\n");
526
+ if (lines.length <= maxLines) return text;
527
+ return lines.slice(0, maxLines).join("\n");
528
+ }
529
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
530
+ return {
531
+ maxChars: Math.max(1, maxChars - suffix.length),
532
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
533
+ };
534
+ }
535
+ function forceSplitBudget(text, budget) {
536
+ const lineCount = countSlackLines(text);
537
+ return {
538
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
539
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
540
+ };
541
+ }
542
+ function getFenceContinuation(text) {
543
+ let open;
544
+ for (const line of text.split("\n")) {
545
+ const trimmed = line.trimStart();
546
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
547
+ if (!openerMatch?.[1]) continue;
548
+ if (!open) {
549
+ open = {
550
+ fence: openerMatch[1],
551
+ openerLine: trimmed
552
+ };
553
+ continue;
554
+ }
555
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
556
+ }
557
+ if (!open) return null;
558
+ return {
559
+ closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
560
+ reopenPrefix: `${open.openerLine}\n`
561
+ };
562
+ }
563
+ function appendSlackSuffix(text, marker) {
564
+ return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
565
+ }
566
+ function stripTrailingContinuationMarker(text) {
567
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
568
+ }
569
+ function takeSlackContinuationChunk(text, budget) {
570
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
571
+ if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
572
+ let carryover = rest ? getFenceContinuation(prefix) : null;
573
+ if (!carryover) return {
574
+ prefix,
575
+ rest
576
+ };
577
+ const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
578
+ ({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
579
+ if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
580
+ carryover = rest ? getFenceContinuation(prefix) : null;
581
+ if (!carryover) return {
582
+ prefix,
583
+ rest
584
+ };
585
+ return {
586
+ prefix,
587
+ rest: `${carryover.reopenPrefix}${rest}`
588
+ };
589
+ }
590
+ /** Return the first continuation-safe chunk plus any remaining text. */
591
+ function takeSlackContinuationPrefix(text, options) {
592
+ const budget = {
593
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
594
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
595
+ };
596
+ const { prefix, rest } = (() => {
597
+ if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
598
+ const initial = takeSlackInlinePrefix(text, budget);
599
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
600
+ })();
601
+ return {
602
+ prefix,
603
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
604
+ rest
605
+ };
606
+ }
607
+ /**
608
+ * Take the largest Slack-safe inline prefix from `text` under the configured
609
+ * character and line budgets. Returns the consumed prefix plus remaining text.
610
+ */
611
+ function takeSlackInlinePrefix(text, options) {
612
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
613
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
614
+ const normalized = text.replace(/\r\n?/g, "\n");
615
+ if (!normalized) return {
616
+ prefix: "",
617
+ rest: ""
618
+ };
619
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
620
+ prefix: normalized,
621
+ rest: ""
622
+ };
623
+ const lineBounded = splitByLineBudget(normalized, maxLines);
624
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
625
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
626
+ if (prefix) return {
627
+ prefix,
628
+ rest: normalized.slice(prefix.length).trimStart()
629
+ };
630
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
631
+ return {
632
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
633
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
634
+ };
635
+ }
636
+ /**
637
+ * Split a normalized Slack reply into multiple inline-safe thread messages.
638
+ *
639
+ * Replies split across more than two messages receive explicit continuation
640
+ * markers on non-final chunks. When
641
+ * `interrupted` is true, the final chunk receives an interruption marker.
642
+ */
643
+ function splitSlackReplyText(text, options) {
644
+ const normalized = normalizeSlackReplyMarkdown(text);
645
+ if (!normalized) return [];
646
+ const chunks = [];
647
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
648
+ let remaining = normalized;
649
+ while (remaining) {
650
+ if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
651
+ chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
652
+ break;
653
+ }
654
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
655
+ ...continuationBudget,
656
+ forceSplit: true
657
+ });
658
+ chunks.push(renderedPrefix);
659
+ remaining = rest;
660
+ }
661
+ if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
662
+ return chunks;
663
+ }
664
+ /**
665
+ * Reserve enough inline budget for a continuation suffix on the current chunk.
666
+ */
667
+ function getSlackContinuationBudget() {
668
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
669
+ }
670
+ // src/chat/slack/reply.ts
671
+ /**
672
+ * Resolve where a Turn posts in Slack.
673
+ *
674
+ * The channel is always the Destination channel. The thread comes from the
675
+ * Turn Source, then the Conversation Location, then the Destination, and each
676
+ * one counts only when it names that channel. Do not derive the target from
677
+ * the Conversation id: an Automation or Watch Conversation can own a Slack
678
+ * thread, and its id is not a Slack thread id.
679
+ */
680
+ function resolveSlackReplyTarget(args) {
681
+ const { channelId } = args.destination;
682
+ const sourceThreadTs = args.source?.kind === "slack" && args.source.channelId === channelId ? args.source.threadTs : void 0;
683
+ const locationThreadTs = args.location?.provider === "slack" && args.location.channelId === channelId ? args.location.threadTs : void 0;
684
+ const threadTs = sourceThreadTs ?? locationThreadTs ?? args.destination.threadTs;
685
+ return {
686
+ channelId,
687
+ ...threadTs ? { threadTs } : void 0
688
+ };
689
+ }
690
+ /**
691
+ * Send one destination-visible Slack reply.
692
+ *
693
+ * Chunks oversized text, builds compact attribution and conversation footer
694
+ * context, and posts through the shared Slack outbound boundary.
695
+ */
696
+ async function sendSlackReply(args) {
697
+ const posts = splitSlackReplyText(args.text).map((text) => ({
698
+ text,
699
+ cards: []
700
+ }));
701
+ const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
702
+ for (let index = 0; index < cards.length; index += 5) {
703
+ const group = cards.slice(index, index + 5);
704
+ const lastPost = posts.at(-1);
705
+ if (index === 0 && lastPost) lastPost.cards = group;
706
+ else posts.push({
707
+ text: "",
708
+ cards: group
709
+ });
710
+ }
711
+ const footer = buildSlackReplyFooter({
712
+ conversationId: args.conversationId,
713
+ replyAttribution: args.replyAttribution
714
+ });
715
+ const messageTs = [];
716
+ let threadTs = args.threadTs;
717
+ for (const [index, post] of posts.entries()) {
718
+ const { text, cards } = post;
719
+ const isFinalChunk = index === posts.length - 1;
720
+ const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
721
+ for (const card of cards) if (!card.entity) blocks.push({
722
+ type: "section",
723
+ text: {
724
+ type: "mrkdwn",
725
+ text: card.text
726
+ }
727
+ });
728
+ if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
729
+ const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
730
+ const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
731
+ const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
732
+ const response = await postSlackMessage({
733
+ channelId: args.channelId,
734
+ threadTs,
735
+ text: fallbackText,
736
+ ...blocks.length ? { blocks } : void 0,
737
+ ...entities.length ? { entities } : void 0
738
+ });
739
+ if (response.ts) {
740
+ messageTs.push(response.ts);
741
+ threadTs ??= response.ts;
742
+ }
743
+ }
744
+ return messageTs;
745
+ }
274
746
  // src/chat/slack/automated-turn-limit-notice.ts
275
747
  /**
276
748
  * Pause notice for the consecutive automated-turn limit.
@@ -301,10 +773,14 @@ async function postAutomatedTurnLimitNoticeForConversation(args) {
301
773
  });
302
774
  return false;
303
775
  }
304
- await postSlackMessage({
305
- channelId: location.channelId,
306
- text: buildAutomatedTurnLimitResponse({ maxTurns: args.maxTurns }),
307
- ...args.threadTs ? { threadTs: args.threadTs } : location.threadTs ? { threadTs: location.threadTs } : void 0
776
+ await sendSlackReply({
777
+ ...resolveSlackReplyTarget({
778
+ destination: location,
779
+ location,
780
+ source: args.source
781
+ }),
782
+ conversationId: args.conversationId,
783
+ text: buildAutomatedTurnLimitResponse({ maxTurns: args.maxTurns })
308
784
  });
309
785
  return true;
310
786
  } catch (error) {
@@ -323,7 +799,7 @@ async function maybePostAutomatedTurnLimitNotice(args) {
323
799
  conversationId: args.conversationId,
324
800
  maxTurns: args.maxTurns,
325
801
  nowMs: args.nowMs,
326
- threadTs: args.threadTs
802
+ source: args.source
327
803
  });
328
804
  }
329
805
  // src/chat/task-input.ts
@@ -913,4 +1389,4 @@ async function skipInputBeforeConversationStop(context) {
913
1389
  await context.attempt.ack();
914
1390
  return { status: "completed" };
915
1391
  }
916
- export { readAutomationResult as A, maybePostAutomatedTurnLimitNotice as C, automationResultSchema as D, FINISH_AUTOMATION_RUN_TOOL_NAME as E, runDispatchOutcome as M, deliversFinalText as O, renderTaskInput as S, resetAutomatedTurnLimit as T, WORKSPACE_SNAPSHOT_FAILED_EVENT as _, EVENT_WAIT_MS as a, workspaceSnapshotWatch as b, isEventMailboxMetadata as c, getEventCatalog as d, SLACK_EVENT_NAMESPACE as f, slackChannelMessageRunsAutomation as g, slackChannelMessageReplyThreadTs as h, skipInputBeforeConversationStop as i, remindMissingAutomationResult as j, finishedRunReply as k, renderEventBatchText as l, slackChannelMessageEvent as m, markConversationsStopped as n, enqueueEventNotification as o, requireSlackChannelMessageScope as p, session_stop_exports as r, eventBatchMessageEvents as s, getConversationStoppedAtMs as t, timerWatchCredentialSubject as u, WORKSPACE_SNAPSHOT_READY_EVENT as v, recordFinishedTurnForAutomatedLimit as w, joinMailboxText as x, workspaceSnapshotFinishedEvent as y };
1392
+ export { automationResultSchema as A, maybePostAutomatedTurnLimitNotice as C, recordFinishedTurnForAutomatedLimit as D, renderSlackObjectCard as E, remindMissingAutomationResult as F, runDispatchOutcome as I, finishedRunReply as M, postsRunNotices as N, resetAutomatedTurnLimit as O, readAutomationResult as P, renderTaskInput as S, sendSlackReply as T, WORKSPACE_SNAPSHOT_FAILED_EVENT as _, EVENT_WAIT_MS as a, workspaceSnapshotWatch as b, isEventMailboxMetadata as c, getEventCatalog as d, SLACK_EVENT_NAMESPACE as f, slackChannelMessageRunsAutomation as g, slackChannelMessageReplyThreadTs as h, skipInputBeforeConversationStop as i, deliversFinalText as j, FINISH_AUTOMATION_RUN_TOOL_NAME as k, renderEventBatchText as l, slackChannelMessageEvent as m, markConversationsStopped as n, enqueueEventNotification as o, requireSlackChannelMessageScope as p, session_stop_exports as r, eventBatchMessageEvents as s, getConversationStoppedAtMs as t, timerWatchCredentialSubject as u, WORKSPACE_SNAPSHOT_READY_EVENT as v, resolveSlackReplyTarget as w, joinMailboxText as x, workspaceSnapshotFinishedEvent as y };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { h as logWarn } from "./logging-DuZ1A4ab.js";
2
+ import { h as logWarn } from "./logging-DLSpkSSr.js";
3
3
  import { s as skillRoots } from "./package-discovery-DJBompdd.js";
4
- import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
4
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-BD9GSpc6.js";
5
5
  import path from "node:path";
6
6
  import { z } from "zod";
7
7
  import fs from "node:fs/promises";
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { dt as actorFromRun, ft as toolInvocationDestination } from "./agent-runner-DgNATN4D.js";
3
- import { n as createAndEnqueueAgentInvocation } from "./work-DUQoNcqP.js";
2
+ import { Bt as actorFromRun, Vt as toolInvocationDestination } from "./agent-runner-CAYfFmU7.js";
3
+ import { n as createAndEnqueueAgentInvocation } from "./work-3N-GHY9m.js";
4
4
  // src/chat/agent-invocations/spawn.ts
5
5
  var spawn_exports = /* @__PURE__ */ __exportAll({ bindSpawnAgent: () => bindSpawnAgent });
6
6
  /** Bind a Run's Actor and credentials to new Agent invocations. */
@@ -1,8 +1,8 @@
1
1
  import { l as TURN_REASONING_LEVELS, s as modelProfileSchema } from "./model-profile-Bg8EoCKD.js";
2
- import { f as getConversationStore, m as getSqlExecutor } from "./viewer-DrN3oC2X.js";
3
- import { a as juniorAgentInvocations, i as juniorAgentBindings, n as AGENT_INVOCATION_PARENT_MAILBOX_STATUSES, t as AGENT_INVOCATION_MAILBOX_STATUSES } from "./agent-invocations-CxKldAeB.js";
4
- import "./executor-VYrx_K4w.js";
5
- import { p as credentialContextSchema } from "./registry-FQutO926.js";
2
+ import { h as getSqlExecutor, p as getConversationStore } from "./viewer-CpXTGlep.js";
3
+ import { a as juniorAgentInvocations, i as juniorAgentBindings, n as AGENT_INVOCATION_PARENT_MAILBOX_STATUSES, t as AGENT_INVOCATION_MAILBOX_STATUSES } from "./agent-invocations-C1RNOCs8.js";
4
+ import "./executor-4vhvsMZJ.js";
5
+ import { p as credentialContextSchema } from "./registry-BgecKva0.js";
6
6
  import { z } from "zod";
7
7
  import { createHash } from "node:crypto";
8
8
  import { and, asc, count, countDistinct, desc, eq, inArray, lt, ne, or, sql } from "drizzle-orm";
@@ -48,6 +48,11 @@ type Usage = z.output<typeof usageSchema>;
48
48
  type AgentTurnUsage = Usage;
49
49
  type ConversationSource = "api" | "internal" | "local" | "plugin" | "event" | "scheduler" | "slack" | "web";
50
50
  type ConversationStatus = "paused" | "failed" | "idle" | "pending" | "running";
51
+ /** Cumulative duration and usage that one Run reported. */
52
+ interface ConversationRunMetrics {
53
+ durationMs: number;
54
+ usage?: AgentTurnUsage;
55
+ }
51
56
  interface ConversationExecution {
52
57
  lastCheckpointAtMs?: number;
53
58
  lastEnqueuedAtMs?: number;
@@ -73,10 +78,9 @@ interface Conversation {
73
78
  createdAtMs: number;
74
79
  destination?: Destination;
75
80
  execution: ConversationExecution;
76
- executionMetrics?: {
77
- durationMs: number;
81
+ /** Metrics of the latest Run that wrote metrics. */
82
+ executionMetrics?: ConversationRunMetrics & {
78
83
  runId?: string;
79
- usage?: AgentTurnUsage;
80
84
  };
81
85
  lastActivityAtMs: number;
82
86
  /** Immutable parent Conversation. */
@@ -115,6 +119,11 @@ interface ConversationStore {
115
119
  get(args: {
116
120
  conversationId: string;
117
121
  }): Promise<Conversation | undefined>;
122
+ /** Read one Run's stored metrics, including a Run that is not the latest. */
123
+ getRunMetrics(args: {
124
+ conversationId: string;
125
+ runId: string;
126
+ }): Promise<ConversationRunMetrics | undefined>;
118
127
  /** Resolve the durable conversation bound to one provider conversation. */
119
128
  getConversationIdByProviderConversation(args: {
120
129
  provider: string;
@@ -180,10 +189,7 @@ interface ConversationStore {
180
189
  destination?: Destination;
181
190
  execution: ConversationExecution;
182
191
  lastActivityAtMs: number;
183
- metrics: {
184
- durationMs: number;
185
- usage?: AgentTurnUsage;
186
- } | null;
192
+ metrics: ConversationRunMetrics | null;
187
193
  actor?: StoredSlackActor;
188
194
  source?: ConversationSource;
189
195
  title?: string;
@@ -1,6 +1,6 @@
1
- import { $t as appendExclusiveInboundMessage, Cn as requestConversationWork$1, En as stopConversationWork$1, Qt as ackMessages$1, Sn as requestAnotherSlice$1, Tn as steerWebPendingMessages$1, _n as markConversationWorkEnqueued$1, an as clearExpiredConversationLease$1, bn as releaseConversationWork$1, cn as deadLetterAttempt$1, dn as getConversationWorkState$1, en as appendInboundMessage, gn as listActiveConversationIds$1, in as clearConsumedConversationWake$1, ln as drainConversationMailbox$1, nn as cancelHumanFacingPendingMessages$1, on as completeConversationStop$1, rn as checkInConversationWork$1, sn as completeConversationWork$1, tn as beginConversationResume$1, un as getConversation$1, vn as recordAttemptFailure$1, wn as startConversationWork$1, xn as removeActiveConversation$1, yn as recordConversationRetry$1 } from "./tasks-0ilmuCux.js";
2
- import { f as getConversationStore } from "./viewer-DrN3oC2X.js";
3
- import "./job-queue-C0BZSa7r.js";
1
+ import { Cn as removeActiveConversation$1, Dn as steerWebPendingMessages$1, En as startConversationWork$1, On as stopConversationWork$1, Sn as releaseConversationWork$1, Tn as requestConversationWork$1, an as checkInConversationWork$1, bn as recordAttemptFailure$1, cn as completeConversationStop$1, dn as drainConversationMailbox$1, en as ackMessages$1, fn as getConversation$1, in as cancelHumanFacingPendingMessages$1, ln as completeConversationWork$1, nn as appendInboundMessage, on as clearConsumedConversationWake$1, pn as getConversationWorkState$1, rn as beginConversationResume$1, sn as clearExpiredConversationLease$1, tn as appendExclusiveInboundMessage, un as deadLetterAttempt$1, vn as listActiveConversationIds$1, wn as requestAnotherSlice$1, xn as recordConversationRetry$1, yn as markConversationWorkEnqueued$1 } from "./tasks-SB9z-VTU.js";
2
+ import { p as getConversationStore } from "./viewer-CpXTGlep.js";
3
+ import "./job-queue-In9QKz-I.js";
4
4
  // src/chat/task-execution/store.ts
5
5
  function metadataStore(options) {
6
6
  return options.conversationStore ?? getConversationStore();