@sentry/junior 0.85.0 → 0.87.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 (49) hide show
  1. package/dist/{agent-hooks-AV7CHLUO.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +779 -356
  3. package/dist/chat/conversation-privacy.d.ts +12 -2
  4. package/dist/chat/conversations/sql/migrations.d.ts +1 -1
  5. package/dist/chat/conversations/sql/store.d.ts +9 -1
  6. package/dist/chat/conversations/store.d.ts +13 -0
  7. package/dist/chat/ingress/message-changed.d.ts +2 -0
  8. package/dist/chat/no-reply.d.ts +7 -0
  9. package/dist/chat/respond.d.ts +7 -1
  10. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  11. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  12. package/dist/chat/runtime/turn.d.ts +7 -0
  13. package/dist/chat/services/persist-retry.d.ts +2 -0
  14. package/dist/chat/services/reply-delivery-plan.d.ts +0 -4
  15. package/dist/chat/services/turn-session-record.d.ts +31 -2
  16. package/dist/chat/slack/client.d.ts +33 -2
  17. package/dist/chat/slack/conversation-context.d.ts +15 -0
  18. package/dist/chat/state/turn-session.d.ts +9 -0
  19. package/dist/chat/task-execution/queue.d.ts +1 -1
  20. package/dist/chat/task-execution/slack-work.d.ts +10 -2
  21. package/dist/chat/task-execution/state.d.ts +44 -4
  22. package/dist/chat/task-execution/store.d.ts +21 -4
  23. package/dist/chat/task-execution/worker.d.ts +11 -4
  24. package/dist/chat/tools/slack/channel-access.d.ts +29 -0
  25. package/dist/chat/tools/slack/thread-read.d.ts +4 -1
  26. package/dist/{chunk-UVNV67EC.js → chunk-2NFV5FMB.js} +4 -4
  27. package/dist/{chunk-QTTTRBNB.js → chunk-6I6HBOQM.js} +80 -15
  28. package/dist/{chunk-ENEWQMRJ.js → chunk-6O5UI3RG.js} +1 -1
  29. package/dist/{chunk-GY7L6VCT.js → chunk-ACJJJEGG.js} +35 -17
  30. package/dist/{chunk-MHEV3T2Y.js → chunk-B6FQPS7A.js} +953 -795
  31. package/dist/{chunk-MTE5NRPJ.js → chunk-BRSQQRG6.js} +1 -1
  32. package/dist/{chunk-EENOFKDN.js → chunk-ENPSU7L7.js} +165 -27
  33. package/dist/{chunk-RUB77TGS.js → chunk-GB5DFM4D.js} +1 -1
  34. package/dist/{chunk-C2PSUWK3.js → chunk-GGD6WK6V.js} +124 -25
  35. package/dist/{chunk-SSUEWAVS.js → chunk-JRXCSSSU.js} +19 -4
  36. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  37. package/dist/{chunk-FYPO4K7C.js → chunk-RIB3M6YA.js} +2 -2
  38. package/dist/{chunk-BU2AWPEN.js → chunk-ZU2ALUVQ.js} +3 -2
  39. package/dist/cli/chat.js +4 -4
  40. package/dist/cli/plugins.js +7 -7
  41. package/dist/cli/snapshot-warmup.js +4 -4
  42. package/dist/cli/upgrade.js +10 -7
  43. package/dist/{db-S3HYBLUS.js → db-SZVUU7RB.js} +4 -4
  44. package/dist/instrumentation.js +1 -1
  45. package/dist/nitro.js +4 -4
  46. package/dist/reporting/conversations.d.ts +2 -1
  47. package/dist/reporting.js +95 -30
  48. package/dist/{runner-IMQH6V2B.js → runner-DMP3IQNM.js} +27 -13
  49. package/package.json +6 -6
@@ -1,14 +1,24 @@
1
1
  export type ConversationPrivacy = "public" | "private";
2
2
  type TraceAttributeValue = string | number | boolean | string[];
3
- /** Resolve whether a conversation may expose raw payloads based on known Slack identity. */
3
+ /**
4
+ * Resolve whether a conversation may expose raw payloads.
5
+ *
6
+ * Only a live source signal or persisted destination visibility can classify
7
+ * a conversation public. Identifier prefixes may only narrow classification
8
+ * toward private. Unknown stays undefined so callers fail closed to private.
9
+ */
4
10
  export declare function resolveConversationPrivacy(input: {
5
11
  channelId?: string;
6
12
  conversationId?: string;
13
+ /** Live source or persisted visibility, when the caller has one. */
14
+ visibility?: ConversationPrivacy;
7
15
  }): ConversationPrivacy | undefined;
8
- /** Gate raw transcript/tool payload exposure to conversations known to be public. */
16
+ /** Gate raw transcript/tool payload exposure to public conversations. */
9
17
  export declare function canExposeConversationPayload(input: {
10
18
  channelId?: string;
11
19
  conversationId?: string;
20
+ /** Live source or persisted visibility, when the caller has one. */
21
+ visibility?: ConversationPrivacy;
12
22
  }): boolean;
13
23
  /** Return the privacy mode bound to the current agent turn. */
14
24
  export declare function getCurrentConversationPrivacy(): ConversationPrivacy | undefined;
@@ -5,7 +5,7 @@ export interface Migration {
5
5
  id: string;
6
6
  statements: readonly string[];
7
7
  }
8
- export declare const migrations: readonly [Migration];
8
+ export declare const migrations: readonly [Migration, Migration];
9
9
  export { schema };
10
10
  /** Apply pending SQL schema migrations for queryable conversation records. */
11
11
  export declare function migrateSchema(executor: JuniorSqlMigrationExecutor, migrationList?: readonly Migration[]): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import type { Destination } from "@sentry/junior-plugin-api";
2
+ import type { ConversationPrivacy } from "@/chat/conversation-privacy";
2
3
  import { type StoredSlackRequester } from "@/chat/requester";
3
4
  import type { JuniorSqlDatabase, JuniorSqlMigrationExecutor } from "@/chat/sql/db";
4
5
  import type { Conversation, ConversationExecution, ConversationSource, ConversationStore } from "../store";
@@ -21,6 +22,7 @@ export declare class SqlStore implements ConversationStore {
21
22
  requester?: StoredSlackRequester;
22
23
  source?: ConversationSource;
23
24
  title?: string;
25
+ visibility?: ConversationPrivacy;
24
26
  }): Promise<void>;
25
27
  recordExecution(args: {
26
28
  channelName?: string;
@@ -33,13 +35,19 @@ export declare class SqlStore implements ConversationStore {
33
35
  source?: ConversationSource;
34
36
  title?: string;
35
37
  updatedAtMs: number;
38
+ visibility?: ConversationPrivacy;
36
39
  }): Promise<void>;
37
40
  /** Copy one conversation record into SQL during backfill. */
38
- backfillConversation(conversation: Conversation): Promise<void>;
41
+ backfillConversation(sourceConversation: Conversation): Promise<void>;
39
42
  listByActivity(args?: {
40
43
  limit?: number;
41
44
  offset?: number;
42
45
  }): Promise<Conversation[]>;
46
+ getDestinationVisibility(args: {
47
+ provider: string;
48
+ providerDestinationId: string;
49
+ providerTenantId?: string;
50
+ }): Promise<ConversationPrivacy | undefined>;
43
51
  /** Serialize all durable mutations for one conversation inside a SQL transaction. */
44
52
  private withConversationMutation;
45
53
  private readConversationRow;
@@ -1,4 +1,5 @@
1
1
  import type { Destination } from "@sentry/junior-plugin-api";
2
+ import type { ConversationPrivacy } from "@/chat/conversation-privacy";
2
3
  import type { StoredSlackRequester } from "@/chat/requester";
3
4
  export type ConversationSource = "api" | "internal" | "local" | "plugin" | "resource_event" | "scheduler" | "slack";
4
5
  export type ConversationStatus = "awaiting_resume" | "failed" | "idle" | "pending" | "running";
@@ -21,12 +22,20 @@ export interface Conversation {
21
22
  source?: ConversationSource;
22
23
  title?: string;
23
24
  updatedAtMs: number;
25
+ /** Persisted destination visibility. Undefined means no destination row exists. */
26
+ visibility?: ConversationPrivacy;
24
27
  }
25
28
  /** Persist and read durable conversation metadata for reporting surfaces. */
26
29
  export interface ConversationStore {
27
30
  get(args: {
28
31
  conversationId: string;
29
32
  }): Promise<Conversation | undefined>;
33
+ /** Read persisted visibility for one destination. Missing rows fail closed. */
34
+ getDestinationVisibility(args: {
35
+ provider: string;
36
+ providerDestinationId: string;
37
+ providerTenantId?: string;
38
+ }): Promise<ConversationPrivacy | undefined>;
30
39
  recordActivity(args: {
31
40
  activityAtMs?: number;
32
41
  channelName?: string;
@@ -36,6 +45,8 @@ export interface ConversationStore {
36
45
  requester?: StoredSlackRequester;
37
46
  source?: ConversationSource;
38
47
  title?: string;
48
+ /** Source-confirmed visibility from the current event's signal only. */
49
+ visibility?: ConversationPrivacy;
39
50
  }): Promise<void>;
40
51
  /** Store task-execution metadata for long-term conversation history. */
41
52
  recordExecution(args: {
@@ -49,6 +60,8 @@ export interface ConversationStore {
49
60
  source?: ConversationSource;
50
61
  title?: string;
51
62
  updatedAtMs: number;
63
+ /** Source-confirmed visibility from the current event's signal only. */
64
+ visibility?: ConversationPrivacy;
52
65
  }): Promise<void>;
53
66
  listByActivity(args?: {
54
67
  limit?: number;
@@ -16,7 +16,9 @@ interface SlackMessageChangedEvent {
16
16
  type: "message";
17
17
  subtype: "message_changed";
18
18
  channel: string;
19
+ channel_type?: string;
19
20
  message: {
21
+ bot_id?: string;
20
22
  files?: SlackEditedMessageFile[];
21
23
  source_team?: string;
22
24
  text?: string;
@@ -0,0 +1,7 @@
1
+ export declare const NO_REPLY_MARKER = "[[NO_REPLY]]";
2
+ /** Detect the reserved marker so Slack side effects can intentionally complete without thread text. */
3
+ export declare function isNoReplyMarker(text: string): boolean;
4
+ /** Detect marker leaks before publication strips or rejects them. */
5
+ export declare function containsNoReplyMarker(text: string): boolean;
6
+ /** Remove the reserved marker from mixed assistant text so it is never shown to users. */
7
+ export declare function stripNoReplyMarker(text: string): string;
@@ -72,7 +72,7 @@ export interface ReplyRequestContext {
72
72
  webSearch?: WebSearchToolDeps;
73
73
  };
74
74
  onStatus?: (status: AssistantStatusSpec) => void | Promise<void>;
75
- drainSteeringMessages?: (inject: (messages: ReplySteeringMessage[]) => Promise<void>) => Promise<ReplySteeringMessage[]>;
75
+ drainSteeringMessages?: (accept: (messages: ReplySteeringMessage[]) => Promise<void>) => Promise<ReplySteeringMessage[]>;
76
76
  /** Return true when the durable worker should pause at the next Pi boundary. */
77
77
  shouldYield?: () => boolean;
78
78
  recordPendingAuth?: (pendingAuth: ConversationPendingAuthState) => void | Promise<void>;
@@ -97,5 +97,11 @@ export interface ReplySteeringMessage {
97
97
  timestampMs?: number;
98
98
  userAttachments?: ReplyRequestAttachment[];
99
99
  }
100
+ /**
101
+ * Convert a mid-run user message into the Pi user message shape used for
102
+ * steering injection and parked-conversation session-log appends, so both
103
+ * paths store identical durable history.
104
+ */
105
+ export declare function buildSteeringPiMessage(message: ReplySteeringMessage): PiMessage;
100
106
  /** Run a full agent turn: discover skills, execute tools, and return the assistant reply. */
101
107
  export declare function generateAssistantReply(messageText: string, context: AssistantReplyRequestContext): Promise<AssistantReply>;
@@ -1,11 +1,3 @@
1
- /**
2
- * Slack reply execution boundary.
3
- *
4
- * This module bridges prepared Slack thread state into `generateAssistantReply`
5
- * and commits the resulting Slack-visible delivery/state updates. It is where
6
- * queued messages, compaction, status updates, and Slack posting meet; agent
7
- * internals stay behind the reply generator.
8
- */
9
1
  import type { Message, Thread } from "chat";
10
2
  import type { SlackAdapter } from "@chat-adapter/slack";
11
3
  import { type Destination } from "@sentry/junior-plugin-api";
@@ -54,13 +46,13 @@ export declare function createReplyToThread(deps: ReplyExecutorDeps): (thread: T
54
46
  beforeFirstResponsePost?: () => Promise<void>;
55
47
  destination: Destination;
56
48
  explicitMention?: boolean;
57
- onInputCommitted?: () => Promise<void>;
49
+ ack?: () => Promise<void>;
58
50
  onToolInvocation?: (invocation: TurnToolInvocation) => void;
59
51
  onTurnCompleted?: () => Promise<void>;
60
52
  onTurnStatePersisted?: () => Promise<void>;
61
53
  preparedState?: PreparedTurnState;
62
54
  queuedMessages?: QueuedTurnMessage[];
63
- drainSteeringMessages?: (inject: (messages: QueuedTurnMessage[]) => Promise<void>, context?: {
55
+ drainSteeringMessages?: (accept: (messages: QueuedTurnMessage[]) => Promise<void>, context?: {
64
56
  conversationContext?: string;
65
57
  }) => Promise<QueuedTurnMessage[]>;
66
58
  shouldYield?: () => boolean;
@@ -26,11 +26,12 @@ export interface SteeringCandidateMessage {
26
26
  }
27
27
  export interface ReplyHooks {
28
28
  beforeFirstResponsePost?: () => Promise<void>;
29
- drainSteeringMessages?: (inject: (messages: SteeringCandidateMessage[]) => Promise<readonly string[] | void>) => Promise<void>;
29
+ drainSteeringMessages?: (accept: (messages: SteeringCandidateMessage[]) => Promise<readonly string[] | void>) => Promise<void>;
30
30
  messageContext?: MessageContext;
31
- onInputCommitted?: () => Promise<void>;
31
+ ack?: () => Promise<void>;
32
32
  onToolInvocation?: (invocation: TurnToolInvocation) => void;
33
33
  onTurnStatePersisted?: () => Promise<void>;
34
+ isFinalAttempt?: boolean;
34
35
  shouldYield?: () => boolean;
35
36
  }
36
37
  interface SteeringDrainContext {
@@ -90,13 +91,13 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
90
91
  beforeFirstResponsePost?: () => Promise<void>;
91
92
  destination: Destination;
92
93
  explicitMention?: boolean;
93
- onInputCommitted?: () => Promise<void>;
94
+ ack?: () => Promise<void>;
94
95
  onToolInvocation?: (invocation: TurnToolInvocation) => void;
95
96
  onTurnCompleted?: () => Promise<void>;
96
97
  onTurnStatePersisted?: () => Promise<void>;
97
98
  preparedState?: TPreparedState;
98
99
  queuedMessages?: QueuedTurnMessage[];
99
- drainSteeringMessages?: (inject: (messages: QueuedTurnMessage[]) => Promise<void>, context?: SteeringDrainContext) => Promise<QueuedTurnMessage[]>;
100
+ drainSteeringMessages?: (accept: (messages: QueuedTurnMessage[]) => Promise<void>, context?: SteeringDrainContext) => Promise<QueuedTurnMessage[]>;
100
101
  shouldYield?: () => boolean;
101
102
  }) => Promise<void>;
102
103
  decideSubscribedReply: SubscribedReplyPolicy;
@@ -51,6 +51,13 @@ export declare class TurnInputCommitLostError extends Error {
51
51
  }
52
52
  /** Return whether an error means the durable worker lost input ownership. */
53
53
  export declare function isTurnInputCommitLostError(error: unknown): error is TurnInputCommitLostError;
54
+ /** Error indicating durable turn input should stay pending for a later worker. */
55
+ export declare class TurnInputDeferredError extends Error {
56
+ readonly code = "turn_input_deferred";
57
+ constructor(message?: string);
58
+ }
59
+ /** Return whether an error means the durable worker should redeliver input later. */
60
+ export declare function isTurnInputDeferredError(error: unknown): error is TurnInputDeferredError;
54
61
  /** Mark a turn as the active turn in conversation state. */
55
62
  export declare function startActiveTurn(args: {
56
63
  conversation: ThreadConversationState;
@@ -0,0 +1,2 @@
1
+ /** Retry a delivered-state persist briefly before surfacing the failure. */
2
+ export declare function persistWithRetry(persist: () => Promise<void>): Promise<void>;
@@ -5,10 +5,6 @@ export interface ReplyDeliveryPlan {
5
5
  postThreadText: boolean;
6
6
  attachFiles: ReplyFileDelivery;
7
7
  }
8
- /** Check if text is a short acknowledgment (emoji, "ok", etc.) that a reaction already covers. */
9
- export declare function isRedundantReactionAckText(text: string): boolean;
10
- /** Check if the user asked for a reaction without also asking for text. */
11
- export declare function isReactionOnlyIntent(text: string): boolean;
12
8
  /** Determine how a reply should be delivered (thread vs channel, file handling). */
13
9
  export declare function buildReplyDeliveryPlan(args: {
14
10
  explicitChannelPostIntent: boolean;
@@ -1,4 +1,5 @@
1
1
  import { type AgentTurnSessionRecord, type AgentTurnSurface } from "@/chat/state/turn-session";
2
+ import type { ConversationPrivacy } from "@/chat/conversation-privacy";
2
3
  import type { Destination, Requester, Source } from "@sentry/junior-plugin-api";
3
4
  import type { PiMessage } from "@/chat/pi/messages";
4
5
  import { type AgentTurnUsage } from "@/chat/usage";
@@ -38,16 +39,26 @@ export declare function persistRunningSessionRecord(args: {
38
39
  surface?: AgentTurnSurface;
39
40
  turnStartMessageIndex?: number;
40
41
  }): Promise<boolean>;
41
- /** Persist a completed turn session record. */
42
+ /**
43
+ * Commit the delivered final reply as the terminal completed session record.
44
+ *
45
+ * Generation completing is not delivery: call this only after the destination
46
+ * accepted the visible final reply, so an undelivered assistant reply never
47
+ * becomes durable conversation history or a terminal completed state. Failures
48
+ * are logged and swallowed because the reply is already user-visible.
49
+ */
42
50
  export declare function persistCompletedSessionRecord(args: {
43
51
  channelName?: string;
44
52
  conversationId: string;
45
53
  currentDurationMs?: number;
46
54
  currentUsage?: AgentTurnUsage;
47
55
  destination?: Destination;
56
+ /** Source-confirmed destination visibility from the current event's signal. */
57
+ destinationVisibility?: ConversationPrivacy;
48
58
  source?: Source;
49
59
  sessionId: string;
50
- sliceId: number;
60
+ /** Defaults to the latest stored slice when the deliverer does not know it. */
61
+ sliceId?: number;
51
62
  allMessages: PiMessage[];
52
63
  loadedSkillNames?: string[];
53
64
  logContext: SessionRecordLogContext;
@@ -55,6 +66,24 @@ export declare function persistCompletedSessionRecord(args: {
55
66
  surface?: AgentTurnSurface;
56
67
  turnStartMessageIndex?: number;
57
68
  }): Promise<void>;
69
+ /** Complete a delivered single-slice run with an explicit session boundary. */
70
+ export declare function completeDeliveredTurn(args: {
71
+ channelName?: string;
72
+ conversationId: string;
73
+ destination: Destination;
74
+ destinationVisibility?: ConversationPrivacy;
75
+ durationMs?: number;
76
+ loadedSkillNames?: string[];
77
+ logContext: SessionRecordLogContext;
78
+ messages: PiMessage[];
79
+ requester?: Requester;
80
+ sessionId: string;
81
+ sliceId: number;
82
+ source: Source;
83
+ surface: AgentTurnSurface;
84
+ turnStartMessageIndex?: number;
85
+ usage?: AgentTurnUsage;
86
+ }): Promise<void>;
58
87
  /**
59
88
  * Persist an auth-pause session record. Returns the durable record only when
60
89
  * the caller can safely hand the user to an authorization resume flow.
@@ -28,18 +28,49 @@ export declare class SlackActionError extends Error {
28
28
  interface SlackRetryContext {
29
29
  action?: string;
30
30
  attributes?: Record<string, string | number | boolean>;
31
+ /**
32
+ * Whether repeating the operation cannot produce a duplicate user-visible
33
+ * effect (reads, deletes, reactions). Request timeouts are ambiguous — Slack
34
+ * may have accepted the write — so they are only retried when the caller
35
+ * marks the operation idempotent. Defaults to false (never risk a duplicate
36
+ * post).
37
+ */
38
+ idempotent?: boolean;
31
39
  /** Extra attributes forwarded onto the per-attempt Sentry span. */
32
40
  spanAttributes?: Record<string, string | number | boolean>;
33
41
  }
34
42
  /** Extract a header value by case-insensitive name from a raw headers object. */
35
43
  export declare function getHeaderString(headers: unknown, name: string): string | undefined;
44
+ /**
45
+ * Bind a workspace installation token for all Slack Web API calls inside
46
+ * `fn`, so multi-workspace outbound writes use the destination workspace's
47
+ * credentials rather than the env fallback token.
48
+ */
49
+ export declare function runWithSlackInstallationToken<T>(token: string, fn: () => T): T;
36
50
  export declare function normalizeSlackConversationId(channelId: string | undefined): string | undefined;
51
+ /**
52
+ * Run a Slack Web API call with bounded retries so transient platform
53
+ * failures do not surface as turn failures, while non-idempotent posts are
54
+ * never re-sent when Slack may already have accepted them.
55
+ *
56
+ * Retry classes: rate limits (bounded Retry-After), connection-phase network
57
+ * failures, and Slack 5xx responses always retry; request timeouts retry only
58
+ * when the caller marks the operation `idempotent`.
59
+ */
37
60
  export declare function withSlackRetries<T>(task: () => Promise<T>, maxAttempts?: number, context?: SlackRetryContext): Promise<T>;
61
+ /**
62
+ * Slack Web API client for the current destination workspace: the ambient
63
+ * installation token when one is bound, otherwise the env bot token for
64
+ * single-workspace deployments. Fails instead of falling back when the call
65
+ * is workspace-scoped and no installation token can be resolved, so a write
66
+ * never goes out with another workspace's credentials.
67
+ */
38
68
  export declare function getSlackClient(): WebClient;
39
69
  /**
40
70
  * Slack channel ID prefixes:
41
- * - C: public channel
42
- * - G: private channel / group DM
71
+ * - C: channel — modern private channels also use C, so the prefix never
72
+ * proves a channel is public
73
+ * - G: legacy private channel / group DM
43
74
  * - D: direct message (1:1)
44
75
  */
45
76
  export declare function isDmChannel(channelId: string): boolean;
@@ -2,11 +2,26 @@
2
2
  export type SlackEventChannelType = "channel" | "group" | "mpim" | "im";
3
3
  /** Slack conversation categories Junior can share with agents. */
4
4
  export type SlackConversationType = "public_channel" | "private_channel" | "group_dm" | "direct_message" | "private_channel_or_group_dm";
5
+ /** Conversation visibility classification derived from source-provided signals. */
6
+ export type SlackConversationVisibility = "public" | "private";
5
7
  /** Slack conversation facts available to the bot for runtime context. */
6
8
  export interface SlackConversationContext {
7
9
  type: SlackConversationType;
8
10
  name?: string;
11
+ /**
12
+ * Visibility proven by a source signal (`channel_type`) or narrowed toward
13
+ * private by a `D`/`G` id prefix. Undefined for `C`-prefixed conversations
14
+ * without a signal, which may be public or private.
15
+ */
16
+ visibility?: SlackConversationVisibility;
9
17
  }
18
+ /**
19
+ * Map Slack's Events API channel_type to a source-confirmed visibility.
20
+ *
21
+ * This is the only mapping allowed to classify a Slack conversation public;
22
+ * channel-id prefixes may only narrow classification toward private.
23
+ */
24
+ export declare function conversationVisibilityFromSlackChannelType(channelType: SlackEventChannelType | undefined): SlackConversationVisibility | undefined;
10
25
  /** Resolve Slack's raw event channel type from a Chat SDK message-like object. */
11
26
  export declare function resolveSlackChannelTypeFromMessage(message: unknown): SlackEventChannelType | undefined;
12
27
  /** Build Slack conversation facts available to runtime consumers. */
@@ -2,6 +2,7 @@ import { type Destination, type Source } from "@sentry/junior-plugin-api";
2
2
  import type { PiMessage } from "@/chat/pi/messages";
3
3
  import { type Requester } from "@/chat/requester";
4
4
  import type { AgentTurnUsage } from "@/chat/usage";
5
+ import type { ConversationPrivacy } from "@/chat/conversation-privacy";
5
6
  import type { ConversationStore } from "@/chat/conversations/store";
6
7
  export type AgentTurnSessionStatus = "running" | "awaiting_resume" | "completed" | "failed" | "abandoned";
7
8
  export type AgentTurnSurface = "slack" | "api" | "scheduler" | "internal";
@@ -40,6 +41,8 @@ export declare function upsertAgentTurnSessionRecord(args: {
40
41
  cumulativeDurationMs?: number;
41
42
  cumulativeUsage?: AgentTurnUsage;
42
43
  destination?: Destination;
44
+ /** Source-confirmed destination visibility from the current event's signal. */
45
+ destinationVisibility?: ConversationPrivacy;
43
46
  source?: Source;
44
47
  lastProgressAtMs?: number;
45
48
  loadedSkillNames?: string[];
@@ -64,6 +67,12 @@ export declare function recordAgentTurnSessionSummary(args: {
64
67
  cumulativeDurationMs?: number;
65
68
  cumulativeUsage?: AgentTurnUsage;
66
69
  destination?: Destination;
70
+ /**
71
+ * Source-confirmed destination visibility from the current event's signal
72
+ * (Slack `channel_type`). Leave unset when no live signal exists so an
73
+ * existing destination visibility is not overwritten.
74
+ */
75
+ destinationVisibility?: ConversationPrivacy;
67
76
  source?: Source;
68
77
  lastProgressAtMs?: number;
69
78
  loadedSkillNames?: string[];
@@ -3,7 +3,7 @@ export interface ConversationQueueMessage {
3
3
  conversationId: string;
4
4
  destination: Destination;
5
5
  }
6
- export type ConversationQueueMessageRejectReason = "destination_mismatch" | "expired" | "malformed" | "signature_mismatch";
6
+ export type ConversationQueueMessageRejectReason = "destination_mismatch" | "expired" | "invalid_record" | "malformed" | "signature_mismatch";
7
7
  export declare class ConversationQueueMessageRejectedError extends Error {
8
8
  conversationId?: string;
9
9
  reason: ConversationQueueMessageRejectReason;
@@ -1,6 +1,6 @@
1
1
  import type { SlackAdapter } from "@chat-adapter/slack";
2
2
  import { Message, ThreadImpl, type SerializedMessage, type SerializedThread, type StateAdapter } from "chat";
3
- import type { SlackTurnRuntime } from "@/chat/runtime/slack-runtime";
3
+ import type { SlackTurnOptions } from "@/chat/runtime/slack-runtime";
4
4
  import type { ConversationStore } from "@/chat/conversations/store";
5
5
  import type { InboundMessage } from "@/chat/task-execution/store";
6
6
  import type { ConversationWorkerContext, ConversationWorkerResult } from "@/chat/task-execution/worker";
@@ -15,6 +15,14 @@ export interface SlackConversationMessageMetadata {
15
15
  route: SlackConversationRoute;
16
16
  thread: SerializedThread;
17
17
  }
18
+ type SlackInboxTurnOptions = SlackTurnOptions & {
19
+ ack: () => Promise<void>;
20
+ isFinalAttempt: boolean;
21
+ };
22
+ interface SlackInboxTurnRuntime {
23
+ handleNewMention(thread: ThreadImpl, message: Message, hooks: SlackInboxTurnOptions): Promise<void>;
24
+ handleSubscribedMessage(thread: ThreadImpl, message: Message, hooks: SlackInboxTurnOptions): Promise<void>;
25
+ }
18
26
  interface SlackResourceEventInboundInput {
19
27
  event: {
20
28
  eventKey: string;
@@ -39,7 +47,7 @@ export interface CreateSlackConversationWorkerOptions {
39
47
  lookupSlackUser?: (teamId: string, userId: string) => Promise<SlackRequesterProfile | null | undefined>;
40
48
  resumeAwaitingContinuation: (conversationId: string) => Promise<boolean>;
41
49
  conversationStore?: ConversationStore;
42
- runtime: Pick<SlackTurnRuntime<unknown>, "handleNewMention" | "handleSubscribedMessage">;
50
+ runtime: SlackInboxTurnRuntime;
43
51
  state?: StateAdapter;
44
52
  }
45
53
  /** Create a Slack mailbox record for a subscribed resource-event notification. */
@@ -1,11 +1,17 @@
1
1
  import type { StateAdapter } from "chat";
2
2
  import type { Destination } from "@sentry/junior-plugin-api";
3
3
  import { type StoredSlackRequester } from "@/chat/requester";
4
+ declare class InvalidConversationRecordError extends Error {
5
+ constructor(conversationId: string);
6
+ }
7
+ /** Return whether an error means a stored conversation record failed validation. */
8
+ export declare function isInvalidConversationRecordError(error: unknown): error is InvalidConversationRecordError;
4
9
  export declare const CONVERSATION_BY_ACTIVITY_INDEX_KEY = "junior:conversation:by-activity";
5
10
  export declare const CONVERSATION_ACTIVE_INDEX_KEY = "junior:conversation:active";
6
11
  export declare const CONVERSATION_WORK_LEASE_TTL_MS = 90000;
7
12
  export declare const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15000;
8
13
  export declare const CONVERSATION_WORK_STALE_ENQUEUE_MS = 60000;
14
+ export declare const CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS = 5;
9
15
  export type Source = "api" | "internal" | "local" | "plugin" | "resource_event" | "scheduler" | "slack";
10
16
  export type ExecutionStatus = "awaiting_resume" | "failed" | "idle" | "pending" | "running";
11
17
  export interface AgentInput {
@@ -15,6 +21,7 @@ export interface AgentInput {
15
21
  text: string;
16
22
  }
17
23
  export interface InboundMessage {
24
+ attemptCount?: number;
18
25
  conversationId: string;
19
26
  createdAtMs: number;
20
27
  destination: Destination;
@@ -88,6 +95,8 @@ export interface AppendAndEnqueueInboundMessageResult extends AppendInboundMessa
88
95
  export interface RequestConversationWorkResult {
89
96
  status: "created" | "updated";
90
97
  }
98
+ /** Whether this is the final attempt before an unacked message is dead-lettered. */
99
+ export declare function isFinalAttempt(message: Pick<InboundMessage, "attemptCount">): boolean;
91
100
  /** Return a persisted conversation record, if one exists. */
92
101
  export declare function getConversation(args: {
93
102
  conversationId: string;
@@ -176,17 +185,17 @@ export declare function checkInConversationWork(args: {
176
185
  * Drain pending mailbox entries after the caller acknowledges durable handling.
177
186
  *
178
187
  * Returning ids acknowledges only that subset; returning nothing acknowledges
179
- * every offered pending entry.
188
+ * every pending entry passed to the handler.
180
189
  */
181
190
  export declare function drainConversationMailbox(args: {
182
191
  conversationId: string;
183
- inject: (messages: InboundMessage[]) => Promise<readonly string[] | void>;
192
+ handle: (messages: InboundMessage[]) => Promise<readonly string[] | void>;
184
193
  leaseToken: string;
185
194
  nowMs?: number;
186
195
  state?: StateAdapter;
187
196
  }): Promise<InboundMessage[]>;
188
- /** Mark selected leased mailbox entries after their session-log injection succeeds. */
189
- export declare function markConversationMessagesInjected(args: {
197
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
198
+ export declare function ackMessages(args: {
190
199
  conversationId: string;
191
200
  inboundMessageIds: string[];
192
201
  leaseToken: string;
@@ -215,6 +224,36 @@ export declare function completeConversationWork(args: {
215
224
  nowMs?: number;
216
225
  state?: StateAdapter;
217
226
  }): Promise<"completed" | "lost_lease" | "pending">;
227
+ /** Failure outcome: `lost_lease` (another owner took over), `recorded` (attempt counted), or `skipped` (durable progress was made). */
228
+ export interface AttemptFailure {
229
+ pendingCount: number;
230
+ deadLetteredMessages: InboundMessage[];
231
+ status: "lost_lease" | "recorded" | "skipped";
232
+ }
233
+ /**
234
+ * Record one failed delivery attempt for the pending messages a run attempted,
235
+ * dead-lettering messages that reach the retry limit so a deterministic
236
+ * failure cannot requeue forever.
237
+ *
238
+ * Attempts are counted only when the run made no durable progress: if any
239
+ * attempted message left the mailbox, the remaining pending entries may be
240
+ * deliberate deferrals and are left untouched. Consumed message ids stay in
241
+ * `inboundMessageIds` so source retries remain duplicates.
242
+ */
243
+ export declare function recordAttemptFailure(args: {
244
+ conversationId: string;
245
+ inboundMessageIds: string[];
246
+ leaseToken: string;
247
+ nowMs?: number;
248
+ state?: StateAdapter;
249
+ }): Promise<AttemptFailure>;
250
+ /** Record a terminal failure completion for a leased conversation. */
251
+ export declare function deadLetterAttempt(args: {
252
+ conversationId: string;
253
+ leaseToken: string;
254
+ nowMs?: number;
255
+ state?: StateAdapter;
256
+ }): Promise<"failed" | "lost_lease" | "pending">;
218
257
  /** Clear an expired durable lease so a later worker can resume safely. */
219
258
  export declare function clearExpiredConversationLease(args: {
220
259
  conversationId: string;
@@ -238,3 +277,4 @@ export declare function listConversationsByActivity(args?: {
238
277
  offset?: number;
239
278
  state?: StateAdapter;
240
279
  }): Promise<Conversation[]>;
280
+ export {};
@@ -2,7 +2,7 @@ import type { StateAdapter } from "chat";
2
2
  import type { ConversationStore } from "@/chat/conversations/store";
3
3
  import type { ConversationWorkQueue } from "./queue";
4
4
  import * as workState from "./state";
5
- export { CONVERSATION_ACTIVE_INDEX_KEY, CONVERSATION_BY_ACTIVITY_INDEX_KEY, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS, CONVERSATION_WORK_LEASE_TTL_MS, CONVERSATION_WORK_STALE_ENQUEUE_MS, type AgentInput, type AppendAndEnqueueInboundMessageResult, type AppendInboundMessageResult, type Conversation, type ConversationExecution, type ConversationWorkLease, type ConversationWorkState, type ExecutionStatus, type InboundMessage, type Lease, type RequestConversationWorkResult, type Source, type StartConversationWorkAcquired, type StartConversationWorkActive, type StartConversationWorkNoWork, type StartConversationWorkResult, } from "@/chat/task-execution/state";
5
+ export { CONVERSATION_ACTIVE_INDEX_KEY, CONVERSATION_BY_ACTIVITY_INDEX_KEY, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS, CONVERSATION_WORK_LEASE_TTL_MS, CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS, CONVERSATION_WORK_STALE_ENQUEUE_MS, isFinalAttempt, isInvalidConversationRecordError, type AgentInput, type AttemptFailure, type AppendAndEnqueueInboundMessageResult, type AppendInboundMessageResult, type Conversation, type ConversationExecution, type ConversationWorkLease, type ConversationWorkState, type ExecutionStatus, type InboundMessage, type Lease, type RequestConversationWorkResult, type Source, type StartConversationWorkAcquired, type StartConversationWorkActive, type StartConversationWorkNoWork, type StartConversationWorkResult, } from "@/chat/task-execution/state";
6
6
  import type { AppendAndEnqueueInboundMessageResult, Conversation, InboundMessage } from "@/chat/task-execution/state";
7
7
  export type EnsureConversationWakeResult = {
8
8
  queueMessageId?: string;
@@ -90,13 +90,13 @@ export declare function checkInConversationWork(args: {
90
90
  nowMs?: number;
91
91
  state?: StateAdapter;
92
92
  }): Promise<boolean>;
93
- /** Drain pending mailbox entries after the caller has durably injected them. */
93
+ /** Drain pending mailbox entries after the caller accepts responsibility. */
94
94
  export declare function drainConversationMailbox(args: Parameters<typeof workState.drainConversationMailbox>[0] & {
95
95
  conversationStore?: ConversationStore;
96
96
  state?: StateAdapter;
97
97
  }): Promise<workState.InboundMessage[]>;
98
- /** Mark selected leased mailbox entries after their session-log injection succeeds. */
99
- export declare function markConversationMessagesInjected(args: {
98
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
99
+ export declare function ackMessages(args: {
100
100
  conversationId: string;
101
101
  inboundMessageIds: string[];
102
102
  leaseToken: string;
@@ -129,6 +129,23 @@ export declare function completeConversationWork(args: {
129
129
  nowMs?: number;
130
130
  state?: StateAdapter;
131
131
  }): Promise<"pending" | "completed" | "lost_lease">;
132
+ /** Record one failed delivery attempt and dead-letter messages at their limit. */
133
+ export declare function recordAttemptFailure(args: {
134
+ conversationId: string;
135
+ inboundMessageIds: string[];
136
+ leaseToken: string;
137
+ conversationStore?: ConversationStore;
138
+ nowMs?: number;
139
+ state?: StateAdapter;
140
+ }): Promise<workState.AttemptFailure>;
141
+ /** Record a terminal failure completion for a leased conversation. */
142
+ export declare function deadLetterAttempt(args: {
143
+ conversationId: string;
144
+ leaseToken: string;
145
+ conversationStore?: ConversationStore;
146
+ nowMs?: number;
147
+ state?: StateAdapter;
148
+ }): Promise<"pending" | "failed" | "lost_lease">;
132
149
  /** Clear an expired durable lease so a later worker can resume safely. */
133
150
  export declare function clearExpiredConversationLease(args: {
134
151
  conversationId: string;
@@ -6,18 +6,25 @@ import { type InboundMessage } from "./store";
6
6
  export declare const CONVERSATION_WORK_DEFER_DELAY_MS = 15000;
7
7
  export declare const CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 240000;
8
8
  export interface ConversationWorkerContext {
9
+ attempt: InboxAttempt;
9
10
  checkIn(): Promise<boolean>;
10
11
  conversationId: string;
11
12
  destination: Destination;
12
- drainMailbox(inject: (messages: InboundMessage[]) => Promise<readonly string[] | void>): Promise<InboundMessage[]>;
13
- leaseToken: string;
14
13
  shouldYield(): boolean;
15
14
  }
15
+ export interface InboxAttempt {
16
+ ack(): Promise<void>;
17
+ conversationId: string;
18
+ destination: Destination;
19
+ drain(handle: (messages: InboundMessage[]) => Promise<readonly string[] | void>): Promise<InboundMessage[]>;
20
+ isFinalAttempt: boolean;
21
+ messages: InboundMessage[];
22
+ }
16
23
  export interface ConversationWorkerResult {
17
- status: "completed" | "lost_lease" | "yielded";
24
+ status: "completed" | "deferred" | "lost_lease" | "yielded";
18
25
  }
19
26
  export interface ConversationWorkProcessResult {
20
- status: "active" | "completed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
27
+ status: "active" | "completed" | "failed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
21
28
  }
22
29
  export interface ProcessConversationWorkOptions {
23
30
  checkInIntervalMs?: number;