@cursor/july 0.1.7 → 0.1.9

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 (210) hide show
  1. package/dist/bin/agent-serve.js +31 -2
  2. package/dist/channels/slack/channel-watch.d.ts +6 -1
  3. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  4. package/dist/channels/slack/channel-watch.js +22 -1
  5. package/dist/channels/slack/cursor-account.d.ts +4 -0
  6. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.js +12 -2
  8. package/dist/channels/slack/defaults.d.ts +10 -5
  9. package/dist/channels/slack/defaults.d.ts.map +1 -1
  10. package/dist/channels/slack/defaults.js +39 -17
  11. package/dist/channels/slack/dispatch.d.ts +18 -0
  12. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  13. package/dist/channels/slack/dispatch.js +141 -38
  14. package/dist/channels/slack/inbound.d.ts +13 -3
  15. package/dist/channels/slack/inbound.d.ts.map +1 -1
  16. package/dist/channels/slack/inbound.js +17 -9
  17. package/dist/channels/slack/index.d.ts +4 -0
  18. package/dist/channels/slack/index.d.ts.map +1 -1
  19. package/dist/channels/slack/index.js +4 -0
  20. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  21. package/dist/channels/slack/live-delivery.js +1 -0
  22. package/dist/channels/slack/manifest.d.ts.map +1 -1
  23. package/dist/channels/slack/manifest.js +3 -0
  24. package/dist/channels/slack/nudge-carrier.d.ts +19 -0
  25. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
  26. package/dist/channels/slack/nudge-carrier.js +166 -0
  27. package/dist/channels/slack/nudge-store.d.ts +61 -0
  28. package/dist/channels/slack/nudge-store.d.ts.map +1 -0
  29. package/dist/channels/slack/nudge-store.js +266 -0
  30. package/dist/channels/slack/post-update-delivery.js +318 -0
  31. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  32. package/dist/channels/slack/slack-channel.js +74 -25
  33. package/dist/channels/slack/stop.d.ts +14 -0
  34. package/dist/channels/slack/stop.d.ts.map +1 -0
  35. package/dist/channels/slack/stop.js +22 -0
  36. package/dist/channels/slack/thread-context.d.ts +81 -0
  37. package/dist/channels/slack/thread-context.d.ts.map +1 -0
  38. package/dist/channels/slack/thread-context.js +470 -0
  39. package/dist/channels/slack/types.d.ts +27 -0
  40. package/dist/channels/slack/types.d.ts.map +1 -1
  41. package/dist/docs/404.html +2 -2
  42. package/dist/docs/ab.html +4 -4
  43. package/dist/docs/assets/{app.COTN7wgo.js → app.Ciosf24M.js} +1 -1
  44. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
  45. package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
  46. package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.CMpbjdfx.js} +2 -2
  47. package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
  48. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
  49. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
  50. package/dist/docs/assets/example-agents_index.md.BPXjUtRB.js +2 -0
  51. package/dist/docs/assets/example-agents_index.md.BPXjUtRB.lean.js +1 -0
  52. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
  53. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
  54. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
  55. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
  56. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
  57. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
  58. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
  59. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
  60. package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.DRTdnFvd.js} +9 -2
  61. package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
  62. package/dist/docs/building-with-agents.html +4 -4
  63. package/dist/docs/concepts.html +4 -4
  64. package/dist/docs/deployment.html +6 -6
  65. package/dist/docs/evals.html +4 -4
  66. package/dist/docs/example-agents/approval-buddy.html +5 -5
  67. package/dist/docs/example-agents/benny.html +5 -5
  68. package/dist/docs/example-agents/bugbot.html +5 -5
  69. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  70. package/dist/docs/example-agents/codeowners-review.html +4 -4
  71. package/dist/docs/example-agents/concierge.html +4 -4
  72. package/dist/docs/example-agents/fsd.html +4 -4
  73. package/dist/docs/example-agents/index.html +6 -6
  74. package/dist/docs/example-agents/knowledge-base.html +4 -4
  75. package/dist/docs/example-agents/oncall.html +34 -0
  76. package/dist/docs/example-agents/security-reviewer.html +6 -6
  77. package/dist/docs/example-agents/slack-agent.html +4 -4
  78. package/dist/docs/example-agents/weather-agent.html +4 -4
  79. package/dist/docs/guides/agent-to-agent.html +4 -4
  80. package/dist/docs/guides/cloud-runtime.html +4 -4
  81. package/dist/docs/guides/github.html +4 -4
  82. package/dist/docs/guides/human-in-the-loop.html +4 -4
  83. package/dist/docs/guides/slack.html +7 -6
  84. package/dist/docs/guides/webhooks.html +4 -4
  85. package/dist/docs/hashmap.json +1 -1
  86. package/dist/docs/hillclimbing.html +4 -4
  87. package/dist/docs/index.html +4 -4
  88. package/dist/docs/quickstart.html +4 -4
  89. package/dist/docs/reference/agent-config.html +4 -4
  90. package/dist/docs/reference/channels.html +4 -4
  91. package/dist/docs/reference/cli.html +4 -4
  92. package/dist/docs/reference/connections.html +4 -4
  93. package/dist/docs/reference/hooks.html +4 -4
  94. package/dist/docs/reference/http-api.html +4 -4
  95. package/dist/docs/reference/instructions.html +4 -4
  96. package/dist/docs/reference/playground.html +4 -4
  97. package/dist/docs/reference/project-layout.html +4 -4
  98. package/dist/docs/reference/schedules.html +4 -4
  99. package/dist/docs/reference/sessions.html +5 -5
  100. package/dist/docs/reference/skills.html +4 -4
  101. package/dist/docs/reference/subagents.html +4 -4
  102. package/dist/docs/reference/tools.html +4 -4
  103. package/dist/docs/scaffolding-agents.html +4 -4
  104. package/dist/docs/storage.html +13 -6
  105. package/dist/docs/troubleshooting.html +4 -4
  106. package/dist/internal/cli-deploy.d.ts.map +1 -1
  107. package/dist/internal/cli-deploy.js +45 -18
  108. package/dist/internal/cli-mcp.d.ts +64 -0
  109. package/dist/internal/cli-mcp.d.ts.map +1 -0
  110. package/dist/internal/cli-mcp.js +570 -0
  111. package/dist/internal/deploy-client.d.ts +7 -1
  112. package/dist/internal/deploy-client.d.ts.map +1 -1
  113. package/dist/internal/deploy-client.js +3 -0
  114. package/dist/internal/deploy-source.d.ts +3 -0
  115. package/dist/internal/deploy-source.d.ts.map +1 -1
  116. package/dist/internal/deploy-source.js +25 -7
  117. package/dist/internal/discovery.js +1 -1
  118. package/dist/internal/distribution.d.ts.map +1 -1
  119. package/dist/internal/distribution.js +3 -0
  120. package/dist/internal/host-kv.d.ts +17 -0
  121. package/dist/internal/host-kv.d.ts.map +1 -0
  122. package/dist/internal/host-kv.js +77 -0
  123. package/dist/internal/hosting.d.ts +12 -2
  124. package/dist/internal/hosting.d.ts.map +1 -1
  125. package/dist/internal/hosting.js +21 -4
  126. package/dist/internal/json-dir-store.js +100 -0
  127. package/dist/internal/logs-client.d.ts +16 -2
  128. package/dist/internal/logs-client.d.ts.map +1 -1
  129. package/dist/internal/logs-client.js +51 -16
  130. package/dist/internal/server.d.ts.map +1 -1
  131. package/dist/internal/server.js +8 -0
  132. package/dist/internal/session-engine.d.ts +42 -7
  133. package/dist/internal/session-engine.d.ts.map +1 -1
  134. package/dist/internal/session-engine.js +276 -66
  135. package/dist/internal/storage-coordinator.d.ts +18 -1
  136. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  137. package/dist/internal/storage-coordinator.js +43 -0
  138. package/dist/internal/workspace.d.ts +19 -0
  139. package/dist/internal/workspace.d.ts.map +1 -1
  140. package/dist/internal/workspace.js +47 -13
  141. package/dist/playground/assets/{index-72vCOBWO.js → index-C0-QEG4O.js} +42 -42
  142. package/dist/playground/assets/index-z3JY9O2x.css +1 -0
  143. package/dist/playground/index.html +2 -2
  144. package/dist/storage-backends/cursor-hosted.d.ts +65 -0
  145. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
  146. package/dist/storage-backends/cursor-hosted.js +203 -0
  147. package/dist/storage-backends/file-kv.d.ts +42 -0
  148. package/dist/storage-backends/file-kv.d.ts.map +1 -0
  149. package/dist/storage-backends/file-kv.js +190 -0
  150. package/dist/storage-backends/postgres-kv.d.ts +82 -0
  151. package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
  152. package/dist/storage-backends/postgres-kv.js +185 -0
  153. package/dist/storage.d.ts +10 -1
  154. package/dist/storage.d.ts.map +1 -1
  155. package/dist/storage.js +10 -1
  156. package/dist/types.d.ts +79 -0
  157. package/dist/types.d.ts.map +1 -1
  158. package/docs/.vitepress/cache/deps/package.json +3 -0
  159. package/docs/deployment.md +7 -3
  160. package/docs/example-agents/approval-buddy.md +1 -1
  161. package/docs/example-agents/index.md +15 -10
  162. package/docs/example-agents/oncall.md +216 -0
  163. package/docs/example-agents/security-reviewer.md +5 -5
  164. package/docs/guides/slack.md +11 -2
  165. package/docs/reference/sessions.md +13 -4
  166. package/docs/storage.md +41 -0
  167. package/package.json +22 -1
  168. package/skills/framework-map/SKILL.md +6 -4
  169. package/src/bin/agent-serve.ts +32 -2
  170. package/src/channels/slack/channel-watch.ts +28 -1
  171. package/src/channels/slack/cursor-account.ts +20 -0
  172. package/src/channels/slack/defaults.ts +51 -20
  173. package/src/channels/slack/dispatch.ts +215 -40
  174. package/src/channels/slack/inbound.ts +30 -10
  175. package/src/channels/slack/index.ts +4 -0
  176. package/src/channels/slack/live-delivery.ts +1 -0
  177. package/src/channels/slack/manifest.ts +3 -0
  178. package/src/channels/slack/nudge-carrier.ts +182 -0
  179. package/src/channels/slack/nudge-store.ts +308 -0
  180. package/src/channels/slack/slack-channel.ts +106 -31
  181. package/src/channels/slack/stop.ts +25 -0
  182. package/src/channels/slack/thread-context.ts +572 -0
  183. package/src/channels/slack/types.ts +27 -0
  184. package/src/internal/cli-deploy.ts +58 -20
  185. package/src/internal/cli-mcp.ts +708 -0
  186. package/src/internal/deploy-client.ts +11 -0
  187. package/src/internal/deploy-source.ts +24 -6
  188. package/src/internal/discovery.ts +1 -1
  189. package/src/internal/distribution.ts +3 -0
  190. package/src/internal/host-kv.ts +85 -0
  191. package/src/internal/hosting.ts +24 -4
  192. package/src/internal/logs-client.ts +67 -21
  193. package/src/internal/server.ts +31 -0
  194. package/src/internal/session-engine.ts +379 -61
  195. package/src/internal/storage-coordinator.ts +41 -0
  196. package/src/internal/workspace.ts +48 -11
  197. package/src/storage-backends/cursor-hosted.ts +246 -0
  198. package/src/storage-backends/file-kv.ts +221 -0
  199. package/src/storage-backends/postgres-kv.ts +253 -0
  200. package/src/storage.ts +11 -1
  201. package/src/types.ts +83 -0
  202. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
  203. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
  204. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +0 -1
  205. package/dist/internal/persistence-coordinator.d.ts +0 -127
  206. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  207. package/dist/persistence.d.ts +0 -184
  208. package/dist/persistence.d.ts.map +0 -1
  209. package/dist/playground/assets/index-BjnMwYoR.css +0 -1
  210. /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Whole-message Slack "stop" directive (Benny parity).
3
+ *
4
+ * A message whose body is only `@bot stop` / `stop` cancels the active turn.
5
+ * The word "stop" embedded in a longer message is not a directive.
6
+ */
7
+
8
+ const MENTION_RE = /^<@[A-Z0-9]+>\s*/i;
9
+
10
+ /** Strip leading Slack user mentions and surrounding whitespace. */
11
+ export function stripLeadingSlackMentions(text: string): string {
12
+ let current = text.trim();
13
+ while (MENTION_RE.test(current)) {
14
+ current = current.replace(MENTION_RE, "").trim();
15
+ }
16
+ return current;
17
+ }
18
+
19
+ /**
20
+ * True when the message (after stripping @mentions) is exactly `stop`
21
+ * (case-insensitive).
22
+ */
23
+ export function isWholeMessageStop(text: string): boolean {
24
+ return stripLeadingSlackMentions(text).toLowerCase() === "stop";
25
+ }
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Fetch prior Slack thread messages and format them for the model prompt.
3
+ *
4
+ * Cold-start mentions (`@agent what happened here?`) only carry the mention
5
+ * text; the parent alert / earlier replies live in `conversations.replies`.
6
+ */
7
+
8
+ import { callSlackApiSoft } from "./api.js";
9
+ import { slackTextToMarkdown } from "./inbound.js";
10
+ import { defaultSlackLog, type SlackLog } from "./log.js";
11
+ import type { SlackBotToken, SoftSlackResult } from "./types.js";
12
+
13
+ /** Cap replies fetched from Slack (plus the parent). */
14
+ const DEFAULT_REPLY_LIMIT = 50;
15
+ /** Soft cap on the formatted block so we don't blow the prompt. */
16
+ const DEFAULT_MAX_CHARS = 12_000;
17
+ /** Slack pages `conversations.replies` oldest-first; bound pagination. */
18
+ const REPLIES_PAGE_LIMIT = 200;
19
+ const REPLIES_MAX_PAGES = 5;
20
+
21
+ function excludeTimestampSet(
22
+ excludeTs: string | readonly string[] | undefined
23
+ ): Set<string> | undefined {
24
+ if (excludeTs === undefined) {
25
+ return undefined;
26
+ }
27
+ if (typeof excludeTs === "string") {
28
+ return new Set([excludeTs]);
29
+ }
30
+ const set = new Set<string>();
31
+ for (const ts of excludeTs) {
32
+ if (ts !== "") {
33
+ set.add(ts);
34
+ }
35
+ }
36
+ return set.size > 0 ? set : undefined;
37
+ }
38
+
39
+ /** Keep the thread root plus the newest replies when over the fetch cap. */
40
+ export function trimThreadMessagesForPrompt(
41
+ messages: JsonRecord[],
42
+ options: {
43
+ threadTs: string;
44
+ limit: number;
45
+ excludeTs?: string | readonly string[];
46
+ }
47
+ ): JsonRecord[] {
48
+ const exclude = excludeTimestampSet(options.excludeTs);
49
+ const filtered = messages.filter((message) => {
50
+ const ts = asString(message.ts);
51
+ if (ts === undefined || ts === "") {
52
+ return false;
53
+ }
54
+ if (exclude !== undefined && exclude.has(ts)) {
55
+ return false;
56
+ }
57
+ return slackApiMessageBody(message).trim() !== "";
58
+ });
59
+ if (filtered.length <= options.limit) {
60
+ return filtered;
61
+ }
62
+ const parentIndex = filtered.findIndex(
63
+ (message) => asString(message.ts) === options.threadTs
64
+ );
65
+ const parent = parentIndex >= 0 ? filtered[parentIndex]! : undefined;
66
+ const withoutParent =
67
+ parentIndex >= 0
68
+ ? filtered.filter((_message, index) => index !== parentIndex)
69
+ : filtered;
70
+ const tail = withoutParent.slice(
71
+ Math.max(
72
+ 0,
73
+ withoutParent.length - (options.limit - (parent === undefined ? 0 : 1))
74
+ )
75
+ );
76
+ if (parent === undefined) {
77
+ return tail;
78
+ }
79
+ if (tail.some((message) => asString(message.ts) === options.threadTs)) {
80
+ return tail;
81
+ }
82
+ return [parent, ...tail];
83
+ }
84
+
85
+ type JsonRecord = Record<string, unknown>;
86
+
87
+ function asRecord(value: unknown): JsonRecord | null {
88
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
89
+ return null;
90
+ }
91
+ return value as JsonRecord;
92
+ }
93
+
94
+ function asString(value: unknown): string | undefined {
95
+ return typeof value === "string" ? value : undefined;
96
+ }
97
+
98
+ /** Pull readable text out of Slack attachment / Block Kit payloads. */
99
+ export function slackApiMessageBody(message: JsonRecord): string {
100
+ const parts: string[] = [];
101
+ const text = asString(message.text);
102
+ if (text !== undefined && text.trim() !== "") {
103
+ parts.push(text);
104
+ }
105
+
106
+ const attachments = Array.isArray(message.attachments)
107
+ ? message.attachments
108
+ : [];
109
+ for (const attachment of attachments) {
110
+ const rec = asRecord(attachment);
111
+ if (rec === null) {
112
+ continue;
113
+ }
114
+ for (const key of [
115
+ "pretext",
116
+ "title",
117
+ "text",
118
+ "fallback",
119
+ "footer",
120
+ ] as const) {
121
+ const value = asString(rec[key]);
122
+ if (value !== undefined && value.trim() !== "") {
123
+ parts.push(value);
124
+ }
125
+ }
126
+ const titleLink = asString(rec.title_link);
127
+ if (titleLink !== undefined && titleLink.trim() !== "") {
128
+ parts.push(titleLink);
129
+ }
130
+ }
131
+
132
+ const blocks = Array.isArray(message.blocks) ? message.blocks : [];
133
+ for (const block of blocks) {
134
+ const rec = asRecord(block);
135
+ if (rec === null) {
136
+ continue;
137
+ }
138
+ const blockText = asRecord(rec.text);
139
+ const blockTextValue = asString(blockText?.text);
140
+ if (blockTextValue !== undefined && blockTextValue.trim() !== "") {
141
+ parts.push(blockTextValue);
142
+ }
143
+ const fields = Array.isArray(rec.fields) ? rec.fields : [];
144
+ for (const field of fields) {
145
+ const fieldRec = asRecord(field);
146
+ const fieldText = asString(fieldRec?.text);
147
+ if (fieldText !== undefined && fieldText.trim() !== "") {
148
+ parts.push(fieldText);
149
+ }
150
+ }
151
+ }
152
+
153
+ // Dedupe identical chunks (Datadog often repeats text/fallback across fields).
154
+ const seen = new Set<string>();
155
+ const deduped: string[] = [];
156
+ for (const part of parts) {
157
+ const trimmed = part.trim();
158
+ if (trimmed === "" || seen.has(trimmed)) {
159
+ continue;
160
+ }
161
+ seen.add(trimmed);
162
+ deduped.push(trimmed);
163
+ }
164
+ return deduped.join("\n");
165
+ }
166
+
167
+ function formatThreadEntry(
168
+ message: JsonRecord,
169
+ role: "parent" | "reply"
170
+ ): string {
171
+ const ts = asString(message.ts) ?? "?";
172
+ const userId = asString(message.user);
173
+ const botId = asString(message.bot_id);
174
+ const username = asString(message.username);
175
+ const who =
176
+ userId !== undefined && userId !== ""
177
+ ? userId
178
+ : botId !== undefined && botId !== ""
179
+ ? `bot:${botId}${username !== undefined ? ` (${username})` : ""}`
180
+ : (username ?? "unknown");
181
+ const body = slackTextToMarkdown(slackApiMessageBody(message));
182
+ if (body === "") {
183
+ return `[${role}] ${who} @ ${ts}:\n(empty)`;
184
+ }
185
+ return `[${role}] ${who} @ ${ts}:\n${body}`;
186
+ }
187
+
188
+ function compareSlackTs(a: string, b: string): number {
189
+ const [aSec = "0", aFrac = "0"] = a.split(".");
190
+ const [bSec = "0", bFrac = "0"] = b.split(".");
191
+ const zero = BigInt(0);
192
+ const sec = BigInt(aSec) - BigInt(bSec);
193
+ if (sec !== zero) {
194
+ return sec < zero ? -1 : 1;
195
+ }
196
+ const frac =
197
+ BigInt(aFrac.padEnd(6, "0").slice(0, 6)) -
198
+ BigInt(bFrac.padEnd(6, "0").slice(0, 6));
199
+ if (frac === zero) {
200
+ return 0;
201
+ }
202
+ return frac < zero ? -1 : 1;
203
+ }
204
+
205
+ /** True when `ts` is strictly after `afterTs`. */
206
+ export function isSlackTsAfter(ts: string, afterTs: string): boolean {
207
+ return compareSlackTs(ts, afterTs) > 0;
208
+ }
209
+
210
+ /**
211
+ * Merge triggering messages (by ts) into a replies window when Slack lag
212
+ * omits them from `conversations.replies`.
213
+ */
214
+ export function mergeThreadMessagesByTs(
215
+ messages: JsonRecord[],
216
+ extras: readonly JsonRecord[]
217
+ ): JsonRecord[] {
218
+ const byTs = new Map<string, JsonRecord>();
219
+ for (const message of messages) {
220
+ const ts = asString(message.ts);
221
+ if (ts !== undefined && ts !== "") {
222
+ byTs.set(ts, message);
223
+ }
224
+ }
225
+ for (const extra of extras) {
226
+ const ts = asString(extra.ts);
227
+ if (ts === undefined || ts === "") {
228
+ continue;
229
+ }
230
+ if (!byTs.has(ts)) {
231
+ byTs.set(ts, extra);
232
+ }
233
+ }
234
+ return [...byTs.values()].sort((a, b) =>
235
+ compareSlackTs(asString(a.ts) ?? "0", asString(b.ts) ?? "0")
236
+ );
237
+ }
238
+
239
+ /**
240
+ * Latest bot-authored message ts in a thread (own posts), used as the warm
241
+ * delta watermark when channel state has not recorded one yet.
242
+ */
243
+ export function findLastBotMessageTs(
244
+ messages: readonly JsonRecord[],
245
+ options?: {
246
+ botUserId?: string;
247
+ botId?: string;
248
+ }
249
+ ): string | undefined {
250
+ const restrictToSelf =
251
+ (options?.botUserId !== undefined && options.botUserId !== "") ||
252
+ (options?.botId !== undefined && options.botId !== "");
253
+ let last: string | undefined;
254
+ for (const message of messages) {
255
+ const ts = asString(message.ts);
256
+ if (ts === undefined || ts === "") {
257
+ continue;
258
+ }
259
+ const userId = asString(message.user);
260
+ const botId = asString(message.bot_id);
261
+ const isBot = restrictToSelf
262
+ ? (options?.botUserId !== undefined &&
263
+ options.botUserId !== "" &&
264
+ userId !== undefined &&
265
+ userId === options.botUserId) ||
266
+ (options?.botId !== undefined &&
267
+ options.botId !== "" &&
268
+ botId !== undefined &&
269
+ botId === options.botId)
270
+ : message.subtype === "bot_message" ||
271
+ (botId !== undefined && botId !== "") ||
272
+ (options?.botUserId !== undefined &&
273
+ userId !== undefined &&
274
+ userId === options.botUserId) ||
275
+ (options?.botId !== undefined &&
276
+ botId !== undefined &&
277
+ botId === options.botId);
278
+ if (!isBot) {
279
+ continue;
280
+ }
281
+ if (last === undefined || isSlackTsAfter(ts, last)) {
282
+ last = ts;
283
+ }
284
+ }
285
+ return last;
286
+ }
287
+
288
+ /** Messages strictly after `afterTs`, excluding listed timestamps. */
289
+ export function selectThreadDeltaMessages(
290
+ messages: JsonRecord[],
291
+ options: {
292
+ afterTs?: string;
293
+ excludeTs?: string | readonly string[];
294
+ }
295
+ ): JsonRecord[] {
296
+ const exclude = excludeTimestampSet(options.excludeTs);
297
+ const afterTs = options.afterTs?.trim() ?? "";
298
+ return messages.filter((message) => {
299
+ const ts = asString(message.ts);
300
+ if (ts === undefined || ts === "") {
301
+ return false;
302
+ }
303
+ if (exclude !== undefined && exclude.has(ts)) {
304
+ return false;
305
+ }
306
+ if (afterTs !== "" && !isSlackTsAfter(ts, afterTs)) {
307
+ return false;
308
+ }
309
+ return slackApiMessageBody(message).trim() !== "";
310
+ });
311
+ }
312
+
313
+ export function formatSlackThreadContextBlock(
314
+ messages: JsonRecord[],
315
+ options?: {
316
+ threadTs?: string;
317
+ excludeTs?: string | readonly string[];
318
+ maxChars?: number;
319
+ }
320
+ ): string | undefined {
321
+ const threadTs = options?.threadTs;
322
+ const maxChars = options?.maxChars ?? DEFAULT_MAX_CHARS;
323
+ const prior =
324
+ threadTs !== undefined && threadTs !== ""
325
+ ? trimThreadMessagesForPrompt(messages, {
326
+ threadTs,
327
+ limit: DEFAULT_REPLY_LIMIT,
328
+ excludeTs: options?.excludeTs,
329
+ })
330
+ : messages.filter((message) => {
331
+ const ts = asString(message.ts);
332
+ if (ts === undefined || ts === "") {
333
+ return false;
334
+ }
335
+ const exclude = excludeTimestampSet(options?.excludeTs);
336
+ if (exclude !== undefined && exclude.has(ts)) {
337
+ return false;
338
+ }
339
+ return slackApiMessageBody(message).trim() !== "";
340
+ });
341
+ if (prior.length === 0) {
342
+ return undefined;
343
+ }
344
+
345
+ const lines = ["<slack_thread>"];
346
+ for (let i = 0; i < prior.length; i++) {
347
+ const ts = asString(prior[i]!.ts);
348
+ const entryRole =
349
+ threadTs !== undefined
350
+ ? ts === threadTs
351
+ ? "parent"
352
+ : "reply"
353
+ : i === 0
354
+ ? "parent"
355
+ : "reply";
356
+ lines.push(formatThreadEntry(prior[i]!, entryRole));
357
+ lines.push("");
358
+ }
359
+ lines.push("</slack_thread>");
360
+
361
+ let block = lines.join("\n").trimEnd();
362
+ if (block.length > maxChars) {
363
+ block = `${block.slice(0, Math.max(0, maxChars - 80))}\n…(truncated; older thread content omitted)\n</slack_thread>`;
364
+ }
365
+ return block;
366
+ }
367
+
368
+ export function formatSlackThreadDeltaBlock(
369
+ messages: JsonRecord[],
370
+ options?: {
371
+ excludeTs?: string | readonly string[];
372
+ afterTs?: string;
373
+ maxChars?: number;
374
+ }
375
+ ): string | undefined {
376
+ const maxChars = options?.maxChars ?? DEFAULT_MAX_CHARS;
377
+ const delta = selectThreadDeltaMessages(messages, {
378
+ afterTs: options?.afterTs,
379
+ excludeTs: options?.excludeTs,
380
+ });
381
+ if (delta.length === 0) {
382
+ return undefined;
383
+ }
384
+ const lines = [
385
+ "<slack_thread_delta>",
386
+ "Thread messages since your last post (or the latest new replies):",
387
+ "",
388
+ ];
389
+ for (const message of delta) {
390
+ lines.push(formatThreadEntry(message, "reply"));
391
+ lines.push("");
392
+ }
393
+ lines.push("</slack_thread_delta>");
394
+ let block = lines.join("\n").trimEnd();
395
+ if (block.length > maxChars) {
396
+ block = `${block.slice(0, Math.max(0, maxChars - 80))}\n…(truncated)\n</slack_thread_delta>`;
397
+ }
398
+ return block;
399
+ }
400
+
401
+ async function fetchThreadMessages(args: {
402
+ botToken: SlackBotToken;
403
+ channelId: string;
404
+ threadTs: string;
405
+ log: SlackLog;
406
+ }): Promise<JsonRecord[] | undefined> {
407
+ const allMessages: JsonRecord[] = [];
408
+ let cursor: string | undefined;
409
+ let exhaustedPageBudget = false;
410
+ try {
411
+ for (let page = 0; page < REPLIES_MAX_PAGES; page++) {
412
+ const result: SoftSlackResult = await callSlackApiSoft({
413
+ botToken: args.botToken,
414
+ operation: "conversations.replies",
415
+ body: {
416
+ channel: args.channelId,
417
+ ts: args.threadTs,
418
+ limit: REPLIES_PAGE_LIMIT,
419
+ inclusive: true,
420
+ ...(cursor === undefined ? {} : { cursor }),
421
+ },
422
+ });
423
+ if (!result.ok) {
424
+ args.log.warn("thread context fetch not-ok", {
425
+ channel: args.channelId,
426
+ thread_ts: args.threadTs,
427
+ error: result.error,
428
+ });
429
+ return undefined;
430
+ }
431
+ const pageMessages = Array.isArray(result.messages)
432
+ ? result.messages
433
+ .map(asRecord)
434
+ .filter((m): m is JsonRecord => m !== null)
435
+ : [];
436
+ allMessages.push(...pageMessages);
437
+ const nextCursor =
438
+ typeof result.response_metadata === "object" &&
439
+ result.response_metadata !== null &&
440
+ !Array.isArray(result.response_metadata)
441
+ ? asString((result.response_metadata as JsonRecord).next_cursor)
442
+ : undefined;
443
+ const hasMore = result.has_more === true;
444
+ cursor =
445
+ nextCursor !== undefined && nextCursor !== "" ? nextCursor : undefined;
446
+ if (!hasMore || cursor === undefined) {
447
+ break;
448
+ }
449
+ if (page === REPLIES_MAX_PAGES - 1) {
450
+ exhaustedPageBudget = true;
451
+ }
452
+ }
453
+ } catch (error) {
454
+ args.log.warn("thread context fetch failed", {
455
+ channel: args.channelId,
456
+ thread_ts: args.threadTs,
457
+ error: error instanceof Error ? error.message : String(error),
458
+ });
459
+ return undefined;
460
+ }
461
+
462
+ if (exhaustedPageBudget) {
463
+ args.log.warn("thread context pagination budget exhausted", {
464
+ channel: args.channelId,
465
+ thread_ts: args.threadTs,
466
+ fetched: allMessages.length,
467
+ });
468
+ }
469
+ return allMessages;
470
+ }
471
+
472
+ /**
473
+ * Load prior messages in a Slack thread and return a prompt context block.
474
+ * Soft-fails (returns undefined) when credentials are missing or the API errors.
475
+ */
476
+ export async function loadSlackThreadContextBlock(args: {
477
+ botToken: SlackBotToken | undefined;
478
+ channelId: string;
479
+ threadTs: string;
480
+ excludeTs?: string | readonly string[];
481
+ log?: SlackLog;
482
+ limit?: number;
483
+ maxChars?: number;
484
+ /** Extra messages merged by ts when Slack replies lag (Benny parity). */
485
+ mergeMessages?: readonly JsonRecord[];
486
+ }): Promise<string | undefined> {
487
+ const log = args.log ?? defaultSlackLog;
488
+ if (args.botToken === undefined) {
489
+ return undefined;
490
+ }
491
+ if (args.channelId === "" || args.threadTs === "") {
492
+ return undefined;
493
+ }
494
+
495
+ const replyLimit = args.limit ?? DEFAULT_REPLY_LIMIT;
496
+ const fetched = await fetchThreadMessages({
497
+ botToken: args.botToken,
498
+ channelId: args.channelId,
499
+ threadTs: args.threadTs,
500
+ log,
501
+ });
502
+ if (fetched === undefined) {
503
+ return undefined;
504
+ }
505
+ const allMessages =
506
+ args.mergeMessages === undefined || args.mergeMessages.length === 0
507
+ ? fetched
508
+ : mergeThreadMessagesByTs(fetched, args.mergeMessages);
509
+
510
+ const trimmed = trimThreadMessagesForPrompt(allMessages, {
511
+ threadTs: args.threadTs,
512
+ limit: replyLimit,
513
+ excludeTs: args.excludeTs,
514
+ });
515
+ return formatSlackThreadContextBlock(trimmed, {
516
+ threadTs: args.threadTs,
517
+ maxChars: args.maxChars,
518
+ });
519
+ }
520
+
521
+ /**
522
+ * Warm-turn delta: messages since the last bot post (or all new replies when
523
+ * the watermark is unknown), with optional merge-by-ts for the triggering ask.
524
+ */
525
+ export async function loadSlackThreadDeltaBlock(args: {
526
+ botToken: SlackBotToken | undefined;
527
+ channelId: string;
528
+ threadTs: string;
529
+ /** Watermark from channel state (`lastBotMessageTs`). */
530
+ afterTs?: string;
531
+ excludeTs?: string | readonly string[];
532
+ log?: SlackLog;
533
+ maxChars?: number;
534
+ mergeMessages?: readonly JsonRecord[];
535
+ botUserId?: string;
536
+ botId?: string;
537
+ }): Promise<string | undefined> {
538
+ const log = args.log ?? defaultSlackLog;
539
+ if (args.botToken === undefined) {
540
+ return undefined;
541
+ }
542
+ if (args.channelId === "" || args.threadTs === "") {
543
+ return undefined;
544
+ }
545
+ const fetched = await fetchThreadMessages({
546
+ botToken: args.botToken,
547
+ channelId: args.channelId,
548
+ threadTs: args.threadTs,
549
+ log,
550
+ });
551
+ if (fetched === undefined) {
552
+ return undefined;
553
+ }
554
+ const allMessages =
555
+ args.mergeMessages === undefined || args.mergeMessages.length === 0
556
+ ? fetched
557
+ : mergeThreadMessagesByTs(fetched, args.mergeMessages);
558
+ const afterTs =
559
+ args.afterTs !== undefined && args.afterTs.trim() !== ""
560
+ ? args.afterTs.trim()
561
+ : findLastBotMessageTs(allMessages, {
562
+ ...(args.botUserId === undefined
563
+ ? {}
564
+ : { botUserId: args.botUserId }),
565
+ ...(args.botId === undefined ? {} : { botId: args.botId }),
566
+ });
567
+ return formatSlackThreadDeltaBlock(allMessages, {
568
+ afterTs,
569
+ excludeTs: args.excludeTs,
570
+ maxChars: args.maxChars,
571
+ });
572
+ }
@@ -62,6 +62,11 @@ export interface SlackChannelState {
62
62
  streamChannelId?: string | null;
63
63
  /** Last cumulative text sent via appendStream (for incremental append). */
64
64
  lastPostedText?: string | null;
65
+ /**
66
+ * Slack message ts of the agent's last delivered reply in this thread.
67
+ * Warm turns use this as the `<slack_thread_delta>` watermark.
68
+ */
69
+ lastBotMessageTs?: string | null;
65
70
  /**
66
71
  * When true, stream APIs failed for this session — use postMessage
67
72
  * for the rest of the turn.
@@ -75,9 +80,12 @@ export interface SlackChannelState {
75
80
  }
76
81
 
77
82
  export interface SlackAuthor {
83
+ /** Slack user id. Empty string for bot posts with no `user` field. */
78
84
  userId: string;
79
85
  userName?: string;
80
86
  isBot: boolean;
87
+ /** Posting app's `bot_id` (`B…`) when the message is bot-authored. */
88
+ botId?: string;
81
89
  }
82
90
 
83
91
  export interface SlackMessage {
@@ -125,6 +133,11 @@ export interface SlackDispatchDecision {
125
133
  * cloud conversation (e.g. per-PR `bc-…` affinity).
126
134
  */
127
135
  sdkAgentId?: string;
136
+ /**
137
+ * Additional Slack message timestamps to omit from `<slack_thread>` hydration
138
+ * (e.g. every message in a coalesced batch already folded into `message`).
139
+ */
140
+ excludeThreadTimestamps?: readonly string[];
128
141
  }
129
142
 
130
143
  export interface SlackSuggestedPrompt {
@@ -191,6 +204,13 @@ export interface SlackChannelPostsOptions {
191
204
  * the Cursor relay does not deliver edits/deletes.
192
205
  */
193
206
  debounceMs?: number;
207
+ /**
208
+ * Also dispatch bot-authored posts (alert feeds, webhook integrations,
209
+ * other agents posting notes). Defaults to `false` — human posts only.
210
+ * The watching bot's own posts are always dropped regardless (loop
211
+ * safety), matched by the app's `bot_id` / bot user id from `auth.test`.
212
+ */
213
+ includeBotPosts?: boolean;
194
214
  }
195
215
 
196
216
  /**
@@ -357,6 +377,13 @@ export interface SlackChannelOptions {
357
377
  */
358
378
  suggestedPrompts?: SlackSuggestedPrompt[];
359
379
  suggestedPromptsTitle?: string;
380
+ /**
381
+ * Rotating `assistant.threads.setStatus` loading lines (Slack caps each
382
+ * entry at 50 chars). Defaults to Thinking… / Working on it… / Almost there….
383
+ */
384
+ loadingMessages?: string[];
385
+ /** Optional teardown (e.g. stop example-local debouncers). Runs after Socket Mode stops. */
386
+ onStop?: () => void | Promise<void>;
360
387
  }
361
388
 
362
389
  export interface SlackManifestOptions {