@mastra/factory 0.24.0 → 0.25.0-alpha.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 (165) hide show
  1. package/README.md +36 -9
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +18 -4
  4. package/dist/auth.js.map +1 -1
  5. package/dist/boards/review.d.ts.map +1 -1
  6. package/dist/boards/review.js +3 -2
  7. package/dist/boards/review.js.map +1 -1
  8. package/dist/factory.d.ts +3 -0
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +61 -69
  11. package/dist/factory.js.map +1 -1
  12. package/dist/integrations/base.d.ts +14 -1
  13. package/dist/integrations/base.d.ts.map +1 -1
  14. package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +4 -3
  15. package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
  16. package/dist/integrations/slack/channel-reply-processor.js +61 -0
  17. package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
  18. package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
  19. package/dist/{session → integrations/slack}/channel-session-link-processor.js +13 -4
  20. package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
  21. package/dist/integrations/slack/integration.d.ts +25 -1
  22. package/dist/integrations/slack/integration.d.ts.map +1 -1
  23. package/dist/integrations/slack/integration.js +55 -5
  24. package/dist/integrations/slack/integration.js.map +1 -1
  25. package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
  26. package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
  27. package/dist/integrations/slack/reply-reminder-processor.js +58 -0
  28. package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
  29. package/dist/integrations/slack/reply-tool.d.ts +31 -0
  30. package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
  31. package/dist/integrations/slack/reply-tool.js +203 -0
  32. package/dist/integrations/slack/reply-tool.js.map +1 -0
  33. package/dist/integrations/slack/share-file-tool.d.ts +45 -0
  34. package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
  35. package/dist/integrations/slack/share-file-tool.js +106 -0
  36. package/dist/integrations/slack/share-file-tool.js.map +1 -0
  37. package/dist/integrations/slack/slack.d.ts.map +1 -1
  38. package/dist/integrations/slack/slack.js +54 -6
  39. package/dist/integrations/slack/slack.js.map +1 -1
  40. package/dist/mcp/events.d.ts +73 -0
  41. package/dist/mcp/events.d.ts.map +1 -0
  42. package/dist/mcp/events.js +158 -0
  43. package/dist/mcp/events.js.map +1 -0
  44. package/dist/mcp/routes.d.ts +0 -1
  45. package/dist/mcp/routes.d.ts.map +1 -1
  46. package/dist/mcp/routes.js +30 -3
  47. package/dist/mcp/routes.js.map +1 -1
  48. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  49. package/dist/platform-auth-host.d.ts.map +1 -1
  50. package/dist/platform-auth-host.js +1 -1
  51. package/dist/platform-auth-host.js.map +1 -1
  52. package/dist/routes/config.d.ts +2 -0
  53. package/dist/routes/config.d.ts.map +1 -1
  54. package/dist/routes/config.js +50 -9
  55. package/dist/routes/config.js.map +1 -1
  56. package/dist/routes/contracts.d.ts +2 -0
  57. package/dist/routes/contracts.d.ts.map +1 -1
  58. package/dist/routes/contracts.js +1 -1
  59. package/dist/routes/contracts.js.map +1 -1
  60. package/dist/routes/session-access.d.ts +7 -2
  61. package/dist/routes/session-access.d.ts.map +1 -1
  62. package/dist/routes/session-access.js +41 -4
  63. package/dist/routes/session-access.js.map +1 -1
  64. package/dist/routes/skills.d.ts.map +1 -1
  65. package/dist/routes/skills.js +4 -3
  66. package/dist/routes/skills.js.map +1 -1
  67. package/dist/routes/source-control-sessions.d.ts +2 -0
  68. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  69. package/dist/routes/source-control-sessions.js +161 -24
  70. package/dist/routes/source-control-sessions.js.map +1 -1
  71. package/dist/routes/surface.d.ts +2 -0
  72. package/dist/routes/surface.d.ts.map +1 -1
  73. package/dist/routes/surface.js +10 -4
  74. package/dist/routes/surface.js.map +1 -1
  75. package/dist/rules/dispatcher.d.ts.map +1 -1
  76. package/dist/rules/dispatcher.js +11 -3
  77. package/dist/rules/dispatcher.js.map +1 -1
  78. package/dist/rules/processor.d.ts +2 -2
  79. package/dist/rules/processor.d.ts.map +1 -1
  80. package/dist/rules/processor.js +3 -3
  81. package/dist/rules/processor.js.map +1 -1
  82. package/dist/sandbox/session-retirement.d.ts +11 -0
  83. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  84. package/dist/sandbox/session-retirement.js +38 -1
  85. package/dist/sandbox/session-retirement.js.map +1 -1
  86. package/dist/sandbox/session-sandbox.d.ts +2 -0
  87. package/dist/sandbox/session-sandbox.d.ts.map +1 -1
  88. package/dist/sandbox/session-sandbox.js +5 -1
  89. package/dist/sandbox/session-sandbox.js.map +1 -1
  90. package/dist/session/credential-policy.d.ts +4 -2
  91. package/dist/session/credential-policy.d.ts.map +1 -1
  92. package/dist/session/credential-policy.js +11 -1
  93. package/dist/session/credential-policy.js.map +1 -1
  94. package/dist/session/factory-session.d.ts.map +1 -1
  95. package/dist/session/factory-session.js +14 -1
  96. package/dist/session/factory-session.js.map +1 -1
  97. package/dist/session/run-audit.d.ts +1 -0
  98. package/dist/session/run-audit.d.ts.map +1 -1
  99. package/dist/session/run-audit.js +7 -2
  100. package/dist/session/run-audit.js.map +1 -1
  101. package/dist/session/session-threads.d.ts +5 -0
  102. package/dist/session/session-threads.d.ts.map +1 -1
  103. package/dist/session/session-threads.js +8 -1
  104. package/dist/session/session-threads.js.map +1 -1
  105. package/dist/session/thinking-default.d.ts +6 -13
  106. package/dist/session/thinking-default.d.ts.map +1 -1
  107. package/dist/session/thinking-default.js +18 -22
  108. package/dist/session/thinking-default.js.map +1 -1
  109. package/dist/session-owner-signal.d.ts +2 -0
  110. package/dist/session-owner-signal.d.ts.map +1 -0
  111. package/dist/session-owner-signal.js +6 -0
  112. package/dist/session-owner-signal.js.map +1 -0
  113. package/dist/skills/service.d.ts +6 -7
  114. package/dist/skills/service.d.ts.map +1 -1
  115. package/dist/skills/service.js +9 -19
  116. package/dist/skills/service.js.map +1 -1
  117. package/dist/storage/domains/audit/actions.d.ts +1 -0
  118. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  119. package/dist/storage/domains/audit/actions.js +2 -1
  120. package/dist/storage/domains/audit/actions.js.map +1 -1
  121. package/dist/storage/domains/audit/base.d.ts +18 -2
  122. package/dist/storage/domains/audit/base.d.ts.map +1 -1
  123. package/dist/storage/domains/audit/base.js +22 -4
  124. package/dist/storage/domains/audit/base.js.map +1 -1
  125. package/dist/storage/domains/model-defaults/base.d.ts +8 -0
  126. package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
  127. package/dist/storage/domains/model-defaults/base.js +18 -0
  128. package/dist/storage/domains/model-defaults/base.js.map +1 -1
  129. package/dist/storage/domains/source-control/base.d.ts +26 -1
  130. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  131. package/dist/storage/domains/source-control/base.js +69 -5
  132. package/dist/storage/domains/source-control/base.js.map +1 -1
  133. package/dist/storage/domains/source-control/inmemory.d.ts +19 -1
  134. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  135. package/dist/storage/domains/source-control/inmemory.js +30 -2
  136. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  137. package/dist/tools/permissions.d.ts.map +1 -1
  138. package/dist/tools/permissions.js +2 -1
  139. package/dist/tools/permissions.js.map +1 -1
  140. package/dist/tools/sandbox-tools.d.ts +12 -0
  141. package/dist/tools/sandbox-tools.d.ts.map +1 -0
  142. package/dist/tools/sandbox-tools.js +40 -0
  143. package/dist/tools/sandbox-tools.js.map +1 -0
  144. package/dist/tools/scope.d.ts.map +1 -1
  145. package/dist/tools/scope.js +2 -5
  146. package/dist/tools/scope.js.map +1 -1
  147. package/dist/tools/session-tools.d.ts.map +1 -1
  148. package/dist/tools/session-tools.js +6 -2
  149. package/dist/tools/session-tools.js.map +1 -1
  150. package/dist/work-item-branch.d.ts +11 -3
  151. package/dist/work-item-branch.d.ts.map +1 -1
  152. package/dist/work-item-branch.js +34 -9
  153. package/dist/work-item-branch.js.map +1 -1
  154. package/dist/workspace.d.ts +2 -1
  155. package/dist/workspace.d.ts.map +1 -1
  156. package/dist/workspace.js +4 -2
  157. package/dist/workspace.js.map +1 -1
  158. package/factory-skills/configure-factory-rules/SKILL.md +2 -2
  159. package/package.json +14 -13
  160. package/dist/session/channel-reply-processor.d.ts.map +0 -1
  161. package/dist/session/channel-reply-processor.js +0 -46
  162. package/dist/session/channel-reply-processor.js.map +0 -1
  163. package/dist/session/channel-session-link-processor.d.ts.map +0 -1
  164. package/dist/session/channel-session-link-processor.js.map +0 -1
  165. /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
@@ -0,0 +1,203 @@
1
+ import { readFactorySessionScope } from "../../rules/binding-context.js";
2
+ import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
3
+ import "./channel-session-link-processor.js";
4
+ import { postTarget, resolveRenderContext } from "./share-file-tool.js";
5
+ import "@mastra/core/channels";
6
+ import { z } from "zod";
7
+ import { createTool } from "@mastra/core/tools";
8
+ //#region src/integrations/slack/reply-tool.ts
9
+ const REPLY_TOOL_NAME = "reply_in_thread";
10
+ /** Set once the first reply of a new session carried the session link. */
11
+ const LINKED_KEY = "factory__replyLinked";
12
+ const replyInput = z.object({ text: z.string().trim().min(1).max(12e3).describe("The message to post, in markdown.") }).strict();
13
+ /**
14
+ * The only way a channel run speaks when the adapter hides reply text
15
+ * (`textDisplay: 'hidden'`). Posting becomes a choice the agent makes, so it
16
+ * stops narrating each step into the thread and can say nothing on purpose.
17
+ * Offered only when the Slack adapter hides reply text and the run's thread is
18
+ * linked to Slack; with text shown the reply stream already posts and this
19
+ * tool would double every message.
20
+ */
21
+ function createReplyTool(requestContext, deps = {}) {
22
+ return { [REPLY_TOOL_NAME]: createTool({
23
+ id: REPLY_TOOL_NAME,
24
+ description: "Post a message in this chat thread. Nothing you write outside this tool reaches the thread, so use it for anything the people there should see: a short note that you are starting a longer job, a question you need answered, and the outcome when you finish. Post when you have something new; do not repeat yourself or post that nothing changed. To stay quiet, do not call it.",
25
+ inputSchema: replyInput,
26
+ execute: async (input, context) => {
27
+ const link = firstReplyLink(requestContext);
28
+ const suffix = link ? `\n\n[${SESSION_LINK_LABEL}](${link})` : "";
29
+ const toolCallId = context?.agent?.toolCallId;
30
+ const streamed = toolCallId ? replyStreams(requestContext).get(toolCallId) : void 0;
31
+ if (toolCallId) replyStreams(requestContext).delete(toolCallId);
32
+ if (!streamed || !await streamed.finish(input.text, suffix)) await (await postTarget(requestContext, deps)).chatThread.post({ markdown: `${input.text}${suffix}` });
33
+ if (link) requestContext.set(LINKED_KEY, true);
34
+ return { posted: true };
35
+ }
36
+ }) };
37
+ }
38
+ /**
39
+ * The session link the first reply of a new session ends with. With reply
40
+ * text hidden, `FactoryChannelSessionLinkProcessor`'s text delta never
41
+ * reaches the thread, so the tool carries the link instead.
42
+ */
43
+ function firstReplyLink(requestContext) {
44
+ if (!requestContext.get("factory__newChannelSession") || requestContext.get(LINKED_KEY)) return void 0;
45
+ const controller = requestContext.get("controller");
46
+ if (!controller?.threadId) return void 0;
47
+ return sessionLink({
48
+ threadId: controller.threadId,
49
+ sessionId: controller.sessionId,
50
+ resourceId: controller.resourceId,
51
+ factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId
52
+ });
53
+ }
54
+ /** Live posts of `reply_in_thread` calls whose text is still streaming in, by tool call id. */
55
+ const STREAMS_KEY = "factory__replyStreams";
56
+ function replyStreams(requestContext) {
57
+ let streams = requestContext.get(STREAMS_KEY);
58
+ if (!streams) {
59
+ streams = /* @__PURE__ */ new Map();
60
+ requestContext.set(STREAMS_KEY, streams);
61
+ }
62
+ return streams;
63
+ }
64
+ /**
65
+ * One streamed Slack post, opened on the first visible text so an abandoned
66
+ * call never leaves an empty message behind.
67
+ */
68
+ function openReplyStream(chatThread) {
69
+ const queue = [];
70
+ let sent = "";
71
+ let closed = false;
72
+ let wake;
73
+ let done;
74
+ async function* chunks() {
75
+ while (true) {
76
+ while (queue.length > 0) yield queue.shift();
77
+ if (closed) return;
78
+ await new Promise((resolve) => wake = resolve);
79
+ }
80
+ }
81
+ const push = (text) => {
82
+ if (!text) return;
83
+ queue.push(text);
84
+ done ??= chatThread.post(chunks()).then(() => true, (error) => {
85
+ console.warn("[factory] streamed reply failed; posting it whole", { error });
86
+ return false;
87
+ });
88
+ wake?.();
89
+ };
90
+ return {
91
+ update(text) {
92
+ const visible = text.trimStart();
93
+ if (!visible.startsWith(sent)) return;
94
+ push(visible.slice(sent.length));
95
+ sent = visible;
96
+ },
97
+ async finish(text, suffix) {
98
+ if (text.startsWith(sent)) push(text.slice(sent.length));
99
+ if (!done) return false;
100
+ push(suffix);
101
+ closed = true;
102
+ wake?.();
103
+ return done;
104
+ },
105
+ async close() {
106
+ closed = true;
107
+ wake?.();
108
+ return done ?? false;
109
+ }
110
+ };
111
+ }
112
+ /**
113
+ * Streams a `reply_in_thread` call's `text` into the thread while the model is
114
+ * still writing it, so a long reply appears as it is written instead of all at
115
+ * once when the call completes. The tool finishes the post with the validated
116
+ * text. Runs whose adapter doesn't stream, or that show reply text, are left
117
+ * alone.
118
+ */
119
+ var FactoryReplyStreamProcessor = class {
120
+ deps;
121
+ id = "factory-reply-stream";
122
+ constructor(deps = {}) {
123
+ this.deps = deps;
124
+ }
125
+ async processOutputStream({ part, state, requestContext }) {
126
+ if (!requestContext) return part;
127
+ const args = state.args ??= {};
128
+ if (part.type === "tool-call-input-streaming-start" && part.payload.toolName === "reply_in_thread") {
129
+ const render = await resolveRenderContext(requestContext, this.deps);
130
+ if (render?.textDisplay !== "hidden" || !render.streaming?.enabled) return part;
131
+ args[part.payload.toolCallId] = "";
132
+ replyStreams(requestContext).set(part.payload.toolCallId, openReplyStream(render.chatThread));
133
+ } else if (part.type === "tool-call-delta" && part.payload.toolCallId in args) {
134
+ const text = partialStringField(args[part.payload.toolCallId] += part.payload.argsTextDelta, "text");
135
+ if (text !== void 0) replyStreams(requestContext).get(part.payload.toolCallId)?.update(text);
136
+ } else if ((part.type === "tool-error" || part.type === "tool-result") && part.payload.toolCallId in args) {
137
+ delete args[part.payload.toolCallId];
138
+ await closeStream(requestContext, part.payload.toolCallId);
139
+ } else if (RUN_END.has(part.type)) {
140
+ for (const toolCallId of Object.keys(args)) await closeStream(requestContext, toolCallId);
141
+ state.args = {};
142
+ }
143
+ return part;
144
+ }
145
+ };
146
+ /** Chunks after which no `reply_in_thread` call of this run will still execute. */
147
+ const RUN_END = /* @__PURE__ */ new Set([
148
+ "finish",
149
+ "abort",
150
+ "error",
151
+ "tripwire"
152
+ ]);
153
+ /**
154
+ * Ends a streamed post whose call will never reach `execute`, keeping the text
155
+ * shown so far, so Slack isn't left with a message that never finishes.
156
+ */
157
+ async function closeStream(requestContext, toolCallId) {
158
+ const streams = replyStreams(requestContext);
159
+ const stream = streams.get(toolCallId);
160
+ if (!stream) return;
161
+ streams.delete(toolCallId);
162
+ await stream.close();
163
+ }
164
+ const ESCAPES = {
165
+ "\"": "\"",
166
+ "\\": "\\",
167
+ "/": "/",
168
+ b: "\b",
169
+ f: "\f",
170
+ n: "\n",
171
+ r: "\r",
172
+ t: " "
173
+ };
174
+ /** The decoded value of a top-level string field in JSON that may still be cut off mid-string. */
175
+ function partialStringField(json, field) {
176
+ const start = new RegExp(`"${field}"\\s*:\\s*"`).exec(json);
177
+ if (!start) return void 0;
178
+ let out = "";
179
+ for (let i = start.index + start[0].length; i < json.length; i++) {
180
+ const ch = json[i];
181
+ if (ch === "\"") return out;
182
+ if (ch !== "\\") {
183
+ out += ch;
184
+ continue;
185
+ }
186
+ const next = json[i + 1];
187
+ if (next === void 0) return out;
188
+ if (next === "u") {
189
+ const hex = json.slice(i + 2, i + 6);
190
+ if (hex.length < 4) return out;
191
+ out += String.fromCharCode(parseInt(hex, 16));
192
+ i += 5;
193
+ } else {
194
+ out += ESCAPES[next] ?? next;
195
+ i += 1;
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+ //#endregion
201
+ export { FactoryReplyStreamProcessor, REPLY_TOOL_NAME, createReplyTool, partialStringField };
202
+
203
+ //# sourceMappingURL=reply-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-tool.js","names":[],"sources":["../../../src/integrations/slack/reply-tool.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from '@mastra/core/channels';\nimport type { ChatChannelRenderContext } from '@mastra/core/channels';\nimport type { Processor, ProcessOutputStreamArgs } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ChunkType } from '@mastra/core/stream';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { readFactorySessionScope } from '../../rules/binding-context.js';\nimport type { IntegrationTools } from '../base.js';\nimport { SESSION_LINK_LABEL, sessionLink } from './channel-reply-processor.js';\nimport { NEW_CHANNEL_SESSION_KEY } from './channel-session-link-processor.js';\nimport { postTarget, resolveRenderContext } from './share-file-tool.js';\nimport type { ShareFileToolDeps } from './share-file-tool.js';\n\nexport const REPLY_TOOL_NAME = 'reply_in_thread';\n\n/** Set once the first reply of a new session carried the session link. */\nconst LINKED_KEY = 'factory__replyLinked';\n\nconst replyInput = z\n .object({\n text: z.string().trim().min(1).max(12_000).describe('The message to post, in markdown.'),\n })\n .strict();\n\n/**\n * The only way a channel run speaks when the adapter hides reply text\n * (`textDisplay: 'hidden'`). Posting becomes a choice the agent makes, so it\n * stops narrating each step into the thread and can say nothing on purpose.\n * Offered only when the Slack adapter hides reply text and the run's thread is\n * linked to Slack; with text shown the reply stream already posts and this\n * tool would double every message.\n */\nexport function createReplyTool(requestContext: RequestContext, deps: ShareFileToolDeps = {}): IntegrationTools {\n return {\n [REPLY_TOOL_NAME]: createTool({\n id: REPLY_TOOL_NAME,\n description:\n 'Post a message in this chat thread. Nothing you write outside this tool reaches the thread, so use it for anything the people there should see: a short note that you are starting a longer job, a question you need answered, and the outcome when you finish. Post when you have something new; do not repeat yourself or post that nothing changed. To stay quiet, do not call it.',\n inputSchema: replyInput,\n execute: async (input, context) => {\n const link = firstReplyLink(requestContext);\n const suffix = link ? `\\n\\n[${SESSION_LINK_LABEL}](${link})` : '';\n // FactoryReplyStreamProcessor may already be streaming this call's text\n // into the thread; finish that post rather than posting it again.\n const toolCallId = context?.agent?.toolCallId;\n const streamed = toolCallId ? replyStreams(requestContext).get(toolCallId) : undefined;\n if (toolCallId) replyStreams(requestContext).delete(toolCallId);\n if (!streamed || !(await streamed.finish(input.text, suffix))) {\n const render = await postTarget(requestContext, deps);\n await render.chatThread.post({ markdown: `${input.text}${suffix}` });\n }\n // Marked only once posted, so a failed first post still gets the link on the retry.\n if (link) requestContext.set(LINKED_KEY, true);\n return { posted: true };\n },\n }),\n };\n}\n\n/**\n * The session link the first reply of a new session ends with. With reply\n * text hidden, `FactoryChannelSessionLinkProcessor`'s text delta never\n * reaches the thread, so the tool carries the link instead.\n */\nfunction firstReplyLink(requestContext: RequestContext): string | undefined {\n if (!requestContext.get(NEW_CHANNEL_SESSION_KEY) || requestContext.get(LINKED_KEY)) return undefined;\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n if (!controller?.threadId) return undefined;\n return sessionLink({\n threadId: controller.threadId,\n sessionId: controller.sessionId,\n resourceId: controller.resourceId,\n factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId,\n });\n}\n\n/** Live posts of `reply_in_thread` calls whose text is still streaming in, by tool call id. */\nconst STREAMS_KEY = 'factory__replyStreams';\n\ninterface ReplyStream {\n /** Show the text decoded from the args so far. */\n update(text: string): void;\n /** End the post with the validated text. Resolves false if the post failed. */\n finish(text: string, suffix: string): Promise<boolean>;\n /** End the post with whatever has been shown, for a call that never ran. */\n close(): Promise<boolean>;\n}\n\nfunction replyStreams(requestContext: RequestContext): Map<string, ReplyStream> {\n let streams = requestContext.get(STREAMS_KEY) as Map<string, ReplyStream> | undefined;\n if (!streams) {\n streams = new Map();\n requestContext.set(STREAMS_KEY, streams);\n }\n return streams;\n}\n\n/**\n * One streamed Slack post, opened on the first visible text so an abandoned\n * call never leaves an empty message behind.\n */\nfunction openReplyStream(chatThread: ChatChannelRenderContext['chatThread']): ReplyStream {\n const queue: string[] = [];\n let sent = '';\n let closed = false;\n let wake: (() => void) | undefined;\n let done: Promise<boolean> | undefined;\n\n async function* chunks(): AsyncGenerator<string> {\n while (true) {\n while (queue.length > 0) yield queue.shift()!;\n if (closed) return;\n await new Promise<void>(resolve => (wake = resolve));\n }\n }\n const push = (text: string) => {\n if (!text) return;\n queue.push(text);\n done ??= chatThread.post(chunks()).then(\n () => true,\n error => {\n console.warn('[factory] streamed reply failed; posting it whole', { error });\n return false;\n },\n );\n wake?.();\n };\n\n return {\n update(text) {\n const visible = text.trimStart();\n if (!visible.startsWith(sent)) return;\n push(visible.slice(sent.length));\n sent = visible;\n },\n async finish(text, suffix) {\n if (text.startsWith(sent)) push(text.slice(sent.length));\n if (!done) return false;\n push(suffix);\n closed = true;\n wake?.();\n return done;\n },\n async close() {\n closed = true;\n wake?.();\n return done ?? false;\n },\n };\n}\n\n/**\n * Streams a `reply_in_thread` call's `text` into the thread while the model is\n * still writing it, so a long reply appears as it is written instead of all at\n * once when the call completes. The tool finishes the post with the validated\n * text. Runs whose adapter doesn't stream, or that show reply text, are left\n * alone.\n */\nexport class FactoryReplyStreamProcessor implements Processor {\n readonly id = 'factory-reply-stream';\n\n constructor(private readonly deps: ShareFileToolDeps = {}) {}\n\n async processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType> {\n if (!requestContext) return part;\n const args = (state.args ??= {}) as Record<string, string>;\n if (part.type === 'tool-call-input-streaming-start' && part.payload.toolName === REPLY_TOOL_NAME) {\n // Resolve the thread the same way the tool does, so a run started from\n // the web UI or woken by a signal streams too, not only a Slack-started one.\n const render = await resolveRenderContext(requestContext, this.deps);\n // A static adapter (`streaming: false`) posts each reply whole, so the tool does too.\n if (render?.textDisplay !== 'hidden' || !render.streaming?.enabled) return part;\n args[part.payload.toolCallId] = '';\n replyStreams(requestContext).set(part.payload.toolCallId, openReplyStream(render.chatThread));\n } else if (part.type === 'tool-call-delta' && part.payload.toolCallId in args) {\n const json = (args[part.payload.toolCallId] += part.payload.argsTextDelta);\n const text = partialStringField(json, 'text');\n if (text !== undefined) replyStreams(requestContext).get(part.payload.toolCallId)?.update(text);\n } else if ((part.type === 'tool-error' || part.type === 'tool-result') && part.payload.toolCallId in args) {\n // A call that failed validation never reaches `execute`; a successful one already finished its post.\n delete args[part.payload.toolCallId];\n await closeStream(requestContext, part.payload.toolCallId);\n } else if (RUN_END.has(part.type)) {\n for (const toolCallId of Object.keys(args)) await closeStream(requestContext, toolCallId);\n state.args = {};\n }\n return part;\n }\n}\n\n/** Chunks after which no `reply_in_thread` call of this run will still execute. */\nconst RUN_END = new Set<string>(['finish', 'abort', 'error', 'tripwire']);\n\n/**\n * Ends a streamed post whose call will never reach `execute`, keeping the text\n * shown so far, so Slack isn't left with a message that never finishes.\n */\nasync function closeStream(requestContext: RequestContext, toolCallId: string): Promise<void> {\n const streams = replyStreams(requestContext);\n const stream = streams.get(toolCallId);\n if (!stream) return;\n streams.delete(toolCallId);\n await stream.close();\n}\n\nconst ESCAPES: Record<string, string> = { '\"': '\"', '\\\\': '\\\\', '/': '/', b: '\\b', f: '\\f', n: '\\n', r: '\\r', t: '\\t' };\n\n/** The decoded value of a top-level string field in JSON that may still be cut off mid-string. */\nexport function partialStringField(json: string, field: string): string | undefined {\n const start = new RegExp(`\"${field}\"\\\\s*:\\\\s*\"`).exec(json);\n if (!start) return undefined;\n let out = '';\n for (let i = start.index + start[0].length; i < json.length; i++) {\n const ch = json[i]!;\n if (ch === '\"') return out;\n if (ch !== '\\\\') {\n out += ch;\n continue;\n }\n const next = json[i + 1];\n if (next === undefined) return out;\n if (next === 'u') {\n const hex = json.slice(i + 2, i + 6);\n if (hex.length < 4) return out;\n out += String.fromCharCode(parseInt(hex, 16));\n i += 5;\n } else {\n out += ESCAPES[next] ?? next;\n i += 1;\n }\n }\n return out;\n}\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { AgentChannels, ChatChannelRenderContext } from '@mastra/core/channels';
2
+ import type { RequestContext } from '@mastra/core/request-context';
3
+ import type { IntegrationTools } from '../base.js';
4
+ /** Slack rejects larger uploads well before this; keep the sandbox read bounded too. */
5
+ export declare const SHARE_FILE_MAX_BYTES: number;
6
+ export interface ShareFileToolDeps {
7
+ /**
8
+ * The channels attached to the controller, used to rebuild the chat thread
9
+ * for runs that did not start from a platform message. Omit when the caller
10
+ * has no controller; the tool is then offered only on inbound-webhook runs.
11
+ */
12
+ channels?: () => AgentChannels | null | undefined;
13
+ }
14
+ /**
15
+ * The chat thread this run replies into, if any.
16
+ *
17
+ * Inbound platform messages stash the render context on `requestContext`.
18
+ * A run woken by a signal (a subagent finishing, a scheduled fire) has no
19
+ * such entry even though its thread is still channel-backed — and that is
20
+ * exactly the run that ends a long job with a file to hand back. Rebuild the
21
+ * context from the thread's persisted channel coordinates the same way
22
+ * `ChatChannelOutputProcessor` does, so the tools post on both kinds of run.
23
+ * A forked subagent's thread carries no channel metadata, so forks get
24
+ * nothing here and never post to the platform.
25
+ */
26
+ export declare function resolveRenderContext(requestContext: RequestContext, deps: ShareFileToolDeps): Promise<ChatChannelRenderContext | undefined>;
27
+ /**
28
+ * Whether this run's thread is a Slack thread, so the channel tools have
29
+ * somewhere to post. A run started from Slack says so on `requestContext`; a
30
+ * web prompt or a woken run on a Slack-linked session costs one thread read.
31
+ * Plain web sessions get `false` and no channel tools.
32
+ */
33
+ export declare function isSlackThread(requestContext: RequestContext, deps: ShareFileToolDeps): Promise<boolean>;
34
+ /** The thread a channel tool posts into, resolved when the tool runs. */
35
+ export declare function postTarget(requestContext: RequestContext, deps: ShareFileToolDeps): Promise<ChatChannelRenderContext>;
36
+ /**
37
+ * Lets a channel-triggered run hand a workspace file back to the thread it
38
+ * came from. The agent's reply stream can only carry text, so a report it
39
+ * generated in the sandbox would otherwise be described by path and left
40
+ * there. Offered only on runs whose thread is linked to a chat thread (see
41
+ * `isSlackThread`); the file is read through the session's workspace
42
+ * filesystem so sandbox confinement still applies.
43
+ */
44
+ export declare function createShareFileTool(requestContext: RequestContext, deps?: ShareFileToolDeps): IntegrationTools;
45
+ //# sourceMappingURL=share-file-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-file-tool.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/share-file-tool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,CAAC;CACnD;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC,CAO/C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ7G;AAED,yEAAyE;AACzE,wBAAsB,UAAU,CAC9B,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,wBAAwB,CAAC,CAInC;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CA4ClH"}
@@ -0,0 +1,106 @@
1
+ import { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from "@mastra/core/channels";
2
+ import { z } from "zod";
3
+ import { createTool } from "@mastra/core/tools";
4
+ //#region src/integrations/slack/share-file-tool.ts
5
+ /** Slack rejects larger uploads well before this; keep the sandbox read bounded too. */
6
+ const SHARE_FILE_MAX_BYTES = 52428800;
7
+ /**
8
+ * Uploads need a scope (`files:write` on Slack) that an app created before
9
+ * this tool existed will not have. `@slack/web-api` reports that as a
10
+ * platform error with `data.error === 'missing_scope'` and the scope in
11
+ * `data.needed`; say so plainly instead of surfacing the raw API error, so
12
+ * the model tells the user what to fix rather than retrying.
13
+ */
14
+ function missingScopeError(error, platform) {
15
+ if (!error || typeof error !== "object") return void 0;
16
+ const data = error.data;
17
+ if (data?.error !== "missing_scope") return void 0;
18
+ const needed = typeof data.needed === "string" && data.needed ? data.needed : "files:write";
19
+ return /* @__PURE__ */ new Error(`The ${platform} app is missing the \`${needed}\` scope, so it cannot upload files. Add the scope to the app and reinstall it, then try again.`);
20
+ }
21
+ /**
22
+ * The chat thread this run replies into, if any.
23
+ *
24
+ * Inbound platform messages stash the render context on `requestContext`.
25
+ * A run woken by a signal (a subagent finishing, a scheduled fire) has no
26
+ * such entry even though its thread is still channel-backed — and that is
27
+ * exactly the run that ends a long job with a file to hand back. Rebuild the
28
+ * context from the thread's persisted channel coordinates the same way
29
+ * `ChatChannelOutputProcessor` does, so the tools post on both kinds of run.
30
+ * A forked subagent's thread carries no channel metadata, so forks get
31
+ * nothing here and never post to the platform.
32
+ */
33
+ async function resolveRenderContext(requestContext, deps) {
34
+ const stashed = requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY);
35
+ if (stashed) return stashed;
36
+ const threadId = requestContext.get("controller")?.threadId;
37
+ if (!threadId) return void 0;
38
+ return await deps.channels?.()?.buildRenderContextForThread(threadId) ?? void 0;
39
+ }
40
+ /**
41
+ * Whether this run's thread is a Slack thread, so the channel tools have
42
+ * somewhere to post. A run started from Slack says so on `requestContext`; a
43
+ * web prompt or a woken run on a Slack-linked session costs one thread read.
44
+ * Plain web sessions get `false` and no channel tools.
45
+ */
46
+ async function isSlackThread(requestContext, deps) {
47
+ const channel = requestContext.get("channel");
48
+ if (channel) return channel.platform === "slack";
49
+ const threadId = requestContext.get("controller")?.threadId;
50
+ if (!threadId) return false;
51
+ return (await deps.channels?.()?.buildChannelContextForThread(threadId))?.channel.platform === "slack";
52
+ }
53
+ /** The thread a channel tool posts into, resolved when the tool runs. */
54
+ async function postTarget(requestContext, deps) {
55
+ const render = await resolveRenderContext(requestContext, deps);
56
+ if (!render) throw new Error("This session is no longer linked to a chat thread.");
57
+ return render;
58
+ }
59
+ /**
60
+ * Lets a channel-triggered run hand a workspace file back to the thread it
61
+ * came from. The agent's reply stream can only carry text, so a report it
62
+ * generated in the sandbox would otherwise be described by path and left
63
+ * there. Offered only on runs whose thread is linked to a chat thread (see
64
+ * `isSlackThread`); the file is read through the session's workspace
65
+ * filesystem so sandbox confinement still applies.
66
+ */
67
+ function createShareFileTool(requestContext, deps = {}) {
68
+ return { attach_file: createTool({
69
+ id: "attach_file",
70
+ description: "Upload a file from the workspace into this chat thread as an attachment. This is the only way the person you are talking to can receive a file you produced (a report, PDF, export, screenshot, image); they cannot open workspace paths. Call it whenever you have a file to hand over, then describe it briefly in your reply. The attachment posts on its own.",
71
+ inputSchema: z.object({ path: z.string().min(1).describe("Absolute path of the file inside the workspace") }),
72
+ execute: async ({ path }, context) => {
73
+ const fs = (requestContext.get("controller")?.workspace ?? context?.workspace)?.filesystem;
74
+ if (!fs) throw new Error("No workspace filesystem is attached to this session.");
75
+ const stat = await fs.stat(path);
76
+ if (stat.type === "directory") throw new Error(`${path} is a directory; attach a single file.`);
77
+ if (stat.size > 52428800) throw new Error(`${path} is ${stat.size} bytes; the limit is ${SHARE_FILE_MAX_BYTES} bytes.`);
78
+ const raw = await fs.readFile(path);
79
+ const data = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
80
+ if (data.length > 52428800) throw new Error(`${path} is ${data.length} bytes; the limit is ${SHARE_FILE_MAX_BYTES} bytes.`);
81
+ const filename = path.split("/").pop() || "file";
82
+ const render = await postTarget(requestContext, deps);
83
+ try {
84
+ await render.chatThread.post({
85
+ markdown: "",
86
+ files: [{
87
+ data,
88
+ filename,
89
+ ...stat.mimeType ? { mimeType: stat.mimeType } : {}
90
+ }]
91
+ });
92
+ } catch (error) {
93
+ throw missingScopeError(error, render.platform) ?? error;
94
+ }
95
+ return {
96
+ attached: true,
97
+ filename,
98
+ bytes: data.length
99
+ };
100
+ }
101
+ }) };
102
+ }
103
+ //#endregion
104
+ export { SHARE_FILE_MAX_BYTES, createShareFileTool, isSlackThread, postTarget, resolveRenderContext };
105
+
106
+ //# sourceMappingURL=share-file-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-file-tool.js","names":[],"sources":["../../../src/integrations/slack/share-file-tool.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from '@mastra/core/channels';\nimport type { AgentChannels, ChatChannelRenderContext } from '@mastra/core/channels';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../base.js';\n\n/** Slack rejects larger uploads well before this; keep the sandbox read bounded too. */\nexport const SHARE_FILE_MAX_BYTES = 50 * 1024 * 1024;\n\nexport interface ShareFileToolDeps {\n /**\n * The channels attached to the controller, used to rebuild the chat thread\n * for runs that did not start from a platform message. Omit when the caller\n * has no controller; the tool is then offered only on inbound-webhook runs.\n */\n channels?: () => AgentChannels | null | undefined;\n}\n\n/**\n * Uploads need a scope (`files:write` on Slack) that an app created before\n * this tool existed will not have. `@slack/web-api` reports that as a\n * platform error with `data.error === 'missing_scope'` and the scope in\n * `data.needed`; say so plainly instead of surfacing the raw API error, so\n * the model tells the user what to fix rather than retrying.\n */\nfunction missingScopeError(error: unknown, platform: string): Error | undefined {\n if (!error || typeof error !== 'object') return undefined;\n const data = (error as { data?: { error?: unknown; needed?: unknown } }).data;\n if (data?.error !== 'missing_scope') return undefined;\n const needed = typeof data.needed === 'string' && data.needed ? data.needed : 'files:write';\n return new Error(\n `The ${platform} app is missing the \\`${needed}\\` scope, so it cannot upload files. Add the scope to the app and reinstall it, then try again.`,\n );\n}\n\n/**\n * The chat thread this run replies into, if any.\n *\n * Inbound platform messages stash the render context on `requestContext`.\n * A run woken by a signal (a subagent finishing, a scheduled fire) has no\n * such entry even though its thread is still channel-backed — and that is\n * exactly the run that ends a long job with a file to hand back. Rebuild the\n * context from the thread's persisted channel coordinates the same way\n * `ChatChannelOutputProcessor` does, so the tools post on both kinds of run.\n * A forked subagent's thread carries no channel metadata, so forks get\n * nothing here and never post to the platform.\n */\nexport async function resolveRenderContext(\n requestContext: RequestContext,\n deps: ShareFileToolDeps,\n): Promise<ChatChannelRenderContext | undefined> {\n const stashed = requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY) as ChatChannelRenderContext | undefined;\n if (stashed) return stashed;\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n const threadId = controller?.threadId;\n if (!threadId) return undefined;\n return (await deps.channels?.()?.buildRenderContextForThread(threadId)) ?? undefined;\n}\n\n/**\n * Whether this run's thread is a Slack thread, so the channel tools have\n * somewhere to post. A run started from Slack says so on `requestContext`; a\n * web prompt or a woken run on a Slack-linked session costs one thread read.\n * Plain web sessions get `false` and no channel tools.\n */\nexport async function isSlackThread(requestContext: RequestContext, deps: ShareFileToolDeps): Promise<boolean> {\n const channel = requestContext.get('channel') as { platform?: string } | undefined;\n if (channel) return channel.platform === 'slack';\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n const threadId = controller?.threadId;\n if (!threadId) return false;\n const linked = await deps.channels?.()?.buildChannelContextForThread(threadId);\n return linked?.channel.platform === 'slack';\n}\n\n/** The thread a channel tool posts into, resolved when the tool runs. */\nexport async function postTarget(\n requestContext: RequestContext,\n deps: ShareFileToolDeps,\n): Promise<ChatChannelRenderContext> {\n const render = await resolveRenderContext(requestContext, deps);\n if (!render) throw new Error('This session is no longer linked to a chat thread.');\n return render;\n}\n\n/**\n * Lets a channel-triggered run hand a workspace file back to the thread it\n * came from. The agent's reply stream can only carry text, so a report it\n * generated in the sandbox would otherwise be described by path and left\n * there. Offered only on runs whose thread is linked to a chat thread (see\n * `isSlackThread`); the file is read through the session's workspace\n * filesystem so sandbox confinement still applies.\n */\nexport function createShareFileTool(requestContext: RequestContext, deps: ShareFileToolDeps = {}): IntegrationTools {\n return {\n attach_file: createTool({\n id: 'attach_file',\n description:\n 'Upload a file from the workspace into this chat thread as an attachment. This is the only way the person you are talking to can receive a file you produced (a report, PDF, export, screenshot, image); they cannot open workspace paths. Call it whenever you have a file to hand over, then describe it briefly in your reply. The attachment posts on its own.',\n inputSchema: z.object({\n path: z.string().min(1).describe('Absolute path of the file inside the workspace'),\n }),\n execute: async ({ path }, context) => {\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n const workspace = controller?.workspace ?? context?.workspace;\n const fs = workspace?.filesystem;\n if (!fs) throw new Error('No workspace filesystem is attached to this session.');\n const stat = await fs.stat(path);\n if (stat.type === 'directory') throw new Error(`${path} is a directory; attach a single file.`);\n // The stat check keeps us from reading something enormous; the check on\n // the bytes we actually hold is the one that bounds the upload, since the\n // file can change between the two calls.\n if (stat.size > SHARE_FILE_MAX_BYTES) {\n throw new Error(`${path} is ${stat.size} bytes; the limit is ${SHARE_FILE_MAX_BYTES} bytes.`);\n }\n const raw = await fs.readFile(path);\n const data = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n if (data.length > SHARE_FILE_MAX_BYTES) {\n throw new Error(`${path} is ${data.length} bytes; the limit is ${SHARE_FILE_MAX_BYTES} bytes.`);\n }\n const filename = path.split('/').pop() || 'file';\n const render = await postTarget(requestContext, deps);\n // File only. The Slack adapter posts any non-empty text as a second\n // message after the upload, so a comment would land as its own bubble\n // between the reply and the file; an empty body returns after the upload.\n try {\n await render.chatThread.post({\n markdown: '',\n files: [{ data, filename, ...(stat.mimeType ? { mimeType: stat.mimeType } : {}) }],\n });\n } catch (error) {\n throw missingScopeError(error, render.platform) ?? error;\n }\n return { attached: true, filename, bytes: data.length };\n },\n }),\n };\n}\n"],"mappings":";;;;;AAUA,MAAa,uBAAuB;;;;;;;;AAkBpC,SAAS,kBAAkB,OAAgB,UAAqC;CAC9E,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO,KAAA;CAChD,MAAM,OAAQ,MAA2D;CACzE,IAAI,MAAM,UAAU,iBAAiB,OAAO,KAAA;CAC5C,MAAM,SAAS,OAAO,KAAK,WAAW,YAAY,KAAK,SAAS,KAAK,SAAS;CAC9E,uBAAO,IAAI,MACT,OAAO,SAAS,wBAAwB,OAAO,gGACjD;AACF;;;;;;;;;;;;;AAcA,eAAsB,qBACpB,gBACA,MAC+C;CAC/C,MAAM,UAAU,eAAe,IAAI,+BAA+B;CAClE,IAAI,SAAS,OAAO;CAEpB,MAAM,WADa,eAAe,IAAI,YACZ,CAAC,EAAE;CAC7B,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,OAAQ,MAAM,KAAK,WAAW,CAAC,EAAE,4BAA4B,QAAQ,KAAM,KAAA;AAC7E;;;;;;;AAQA,eAAsB,cAAc,gBAAgC,MAA2C;CAC7G,MAAM,UAAU,eAAe,IAAI,SAAS;CAC5C,IAAI,SAAS,OAAO,QAAQ,aAAa;CAEzC,MAAM,WADa,eAAe,IAAI,YACZ,CAAC,EAAE;CAC7B,IAAI,CAAC,UAAU,OAAO;CAEtB,QAAO,MADc,KAAK,WAAW,CAAC,EAAE,6BAA6B,QAAQ,EAAA,EAC9D,QAAQ,aAAa;AACtC;;AAGA,eAAsB,WACpB,gBACA,MACmC;CACnC,MAAM,SAAS,MAAM,qBAAqB,gBAAgB,IAAI;CAC9D,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,oDAAoD;CACjF,OAAO;AACT;;;;;;;;;AAUA,SAAgB,oBAAoB,gBAAgC,OAA0B,CAAC,GAAqB;CAClH,OAAO,EACL,aAAa,WAAW;EACtB,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO,EACpB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,gDAAgD,EACnF,CAAC;EACD,SAAS,OAAO,EAAE,QAAQ,YAAY;GAGpC,MAAM,MAFa,eAAe,IAAI,YACX,CAAC,EAAE,aAAa,SAAS,UAAA,EAC9B;GACtB,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,sDAAsD;GAC/E,MAAM,OAAO,MAAM,GAAG,KAAK,IAAI;GAC/B,IAAI,KAAK,SAAS,aAAa,MAAM,IAAI,MAAM,GAAG,KAAK,uCAAuC;GAI9F,IAAI,KAAK,OAAA,UACP,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM,KAAK,KAAK,uBAAuB,qBAAqB,QAAQ;GAE9F,MAAM,MAAM,MAAM,GAAG,SAAS,IAAI;GAClC,MAAM,OAAO,OAAO,SAAS,GAAG,IAAI,MAAM,OAAO,KAAK,GAAG;GACzD,IAAI,KAAK,SAAA,UACP,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM,KAAK,OAAO,uBAAuB,qBAAqB,QAAQ;GAEhG,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GAC1C,MAAM,SAAS,MAAM,WAAW,gBAAgB,IAAI;GAIpD,IAAI;IACF,MAAM,OAAO,WAAW,KAAK;KAC3B,UAAU;KACV,OAAO,CAAC;MAAE;MAAM;MAAU,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;KAAG,CAAC;IACnF,CAAC;GACH,SAAS,OAAO;IACd,MAAM,kBAAkB,OAAO,OAAO,QAAQ,KAAK;GACrD;GACA,OAAO;IAAE,UAAU;IAAM;IAAU,OAAO,KAAK;GAAO;EACxD;CACF,CAAC,EACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/slack.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/slack.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@ import { readRequestContextOrgId, seedSessionOrg } from "../../session/org-seed.
6
6
  import { prepareSessionRunContext } from "../subscription-session.js";
7
7
  import { FactorySourceControlConflictError, createSourceControlSessionLookup, hydrateFactorySession, resolveFactorySessionModel, resolveFactorySourceControl, resolvePrimaryEnvironmentRepository } from "../../session/factory-session.js";
8
8
  import { FACTORY_SLACK_SENDER_KEY } from "../../tools/scope.js";
9
- import { markNewChannelSession } from "../../session/channel-session-link-processor.js";
9
+ import { markNewChannelSession } from "./channel-session-link-processor.js";
10
10
  import { resolveEmojiShortcodes } from "./emoji.js";
11
11
  import { slackCommentSource } from "./feed-publisher.js";
12
12
  import { ChannelSessionRejectedError } from "@mastra/core/channels";
@@ -455,12 +455,12 @@ async function findInternalThread(mastra, thread) {
455
455
  } },
456
456
  perPage: 1
457
457
  }) ?? { threads: [] };
458
- return threads[0];
458
+ return threads[0] ?? null;
459
459
  }
460
460
  async function prepareExistingSessionOwnerContext(thread, deps, ctx, options) {
461
461
  const sourceControls = configuredSourceControls(deps);
462
462
  if (sourceControls.length === 0 && !options.requireInternalThread) return "ready";
463
- const internalThread = await findInternalThread(ctx.mastra, thread);
463
+ const internalThread = options.internalThread !== void 0 ? options.internalThread : await findInternalThread(ctx.mastra, thread);
464
464
  if (!internalThread) {
465
465
  if (options.requireInternalThread) throw new Error(`Could not resolve the internal Slack thread for ${thread.id}.`);
466
466
  return "ready";
@@ -506,7 +506,8 @@ async function gateDispatch(thread, message, deps, ctx, options = { requireInter
506
506
  const ownerContext = await prepareExistingSessionOwnerContext(thread, deps, ctx, {
507
507
  expectedOrgId: sender.link.orgId,
508
508
  senderUserId: sender.link.userId,
509
- requireInternalThread: options.requireInternalThread
509
+ requireInternalThread: options.requireInternalThread,
510
+ internalThread: options.internalThread
510
511
  });
511
512
  if (ownerContext === "access-denied") {
512
513
  await thread.post("This private Factory session belongs to another user.");
@@ -687,19 +688,66 @@ const createHandlers = (deps) => {
687
688
  return {
688
689
  onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {
689
690
  if (/^aside\b/i.test(message.text)) return ingestAside(thread, message, deps);
691
+ const internalThread = await findInternalThread(ctx.mastra, thread);
692
+ if (!message.isMention && internalThread?.metadata?.[MUTED_KEY] === "true") return;
690
693
  try {
691
- if (!await gateDispatch(thread, message, deps, ctx, { requireInternalThread: true })) return;
694
+ if (!await gateDispatch(thread, message, deps, ctx, {
695
+ requireInternalThread: true,
696
+ internalThread
697
+ })) return;
692
698
  } catch (error) {
693
699
  await reportPreDispatchError(thread, message, ctx, error);
694
700
  return;
695
701
  }
702
+ const command = message.isMention ? muteCommand(message) : void 0;
703
+ if (command && internalThread) return setThreadMuted(thread, internalThread, message, ctx, command === "mute");
696
704
  acknowledgeReceipt(thread, deps);
697
705
  await defaultHandler(thread, message);
698
706
  },
699
- onMention: newSessionChatHandler,
707
+ onMention: async (thread, message, defaultHandler, ctx) => {
708
+ if (muteCommand(message)) return;
709
+ await newSessionChatHandler(thread, message, defaultHandler, ctx);
710
+ },
700
711
  onDirectMessage: newSessionChatHandler
701
712
  };
702
713
  };
714
+ /** Thread metadata key; one Factory runs one Slack app, so this is per bot. */
715
+ const MUTED_KEY = "factory_muted";
716
+ /**
717
+ * `mute` or `unmute` when that word, with the mentions taken out, is the whole
718
+ * message. Reads Slack's raw text, where a mention is still `<@U…>`: the
719
+ * normalized `message.text` spells it as the bot's display name, which can
720
+ * hold spaces and punctuation (`@Mastra Factory(Caleb)`).
721
+ */
722
+ function muteCommand(message) {
723
+ const raw = message.raw;
724
+ const rest = (typeof raw?.text === "string" ? raw.text : message.text).replace(/<@[^>]+>/g, " ").replace(/(?<!\w)@[\w.-]+/g, " ").trim().replace(/[.!]+$/, "").toLowerCase();
725
+ return rest === "mute" || rest === "unmute" ? rest : void 0;
726
+ }
727
+ /**
728
+ * Stored on the Mastra thread next to `channel_subscribed`, so a mute holds
729
+ * across deploys and replicas. Callers run the sender's link and session
730
+ * access gate first. A reaction confirms it without posting.
731
+ */
732
+ async function setThreadMuted(thread, internal, message, ctx, muted) {
733
+ const store = await ctx.mastra?.getStorage()?.getStore("memory");
734
+ if (!store) return;
735
+ await store.patchThread({
736
+ id: internal.id,
737
+ metadata: {
738
+ ...internal.metadata,
739
+ [MUTED_KEY]: String(muted)
740
+ }
741
+ });
742
+ try {
743
+ await thread.adapter.addReaction(thread.id, message.id, muted ? "mute" : "loud_sound");
744
+ } catch (error) {
745
+ console.warn("[slack] mute reaction failed", {
746
+ thread: thread.id,
747
+ error
748
+ });
749
+ }
750
+ }
703
751
  function createSlackChannelsConfig(deps) {
704
752
  const adapter = createSlackAdapter({
705
753
  ...deps.slack,