@cursor/july 0.1.61 → 0.1.68

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 (196) hide show
  1. package/dist/bin/agent-serve.js +8 -0
  2. package/dist/channels/github/github-channel.d.ts +1 -1
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +14 -1
  5. package/dist/channels/github/types.d.ts +2 -0
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/slack/channel-watch.d.ts +2 -1
  8. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  9. package/dist/channels/slack/channel-watch.js +7 -3
  10. package/dist/channels/slack/dispatch.d.ts +9 -0
  11. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  12. package/dist/channels/slack/dispatch.js +41 -21
  13. package/dist/channels/slack/manifest.d.ts.map +1 -1
  14. package/dist/channels/slack/manifest.js +1 -0
  15. package/dist/channels/slack/nudge-store.d.ts +16 -0
  16. package/dist/channels/slack/nudge-store.d.ts.map +1 -1
  17. package/dist/channels/slack/nudge-store.js +53 -0
  18. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  19. package/dist/channels/slack/slack-channel.js +4 -1
  20. package/dist/channels/slack/types.d.ts +22 -5
  21. package/dist/channels/slack/types.d.ts.map +1 -1
  22. package/dist/docs/404.html +2 -2
  23. package/dist/docs/ab.html +3 -3
  24. package/dist/docs/assets/{app.CW3KJh72.js → app.C5XUTCw9.js} +1 -1
  25. package/dist/docs/assets/chunks/@localSearchIndexroot.DvUR3lv9.js +1 -0
  26. package/dist/docs/assets/chunks/{VPLocalSearchBox.Bk6EWpPj.js → VPLocalSearchBox.CGDTAYHY.js} +1 -1
  27. package/dist/docs/assets/chunks/{theme.Bz9AodBA.js → theme.lPixh250.js} +2 -2
  28. package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Bwswbm6H.js} +13 -3
  29. package/dist/docs/assets/{reference_connections.md.CSQiNzre.lean.js → reference_connections.md.Bwswbm6H.lean.js} +1 -1
  30. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.Wmvb7Lob.js} +2 -2
  31. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.Wmvb7Lob.lean.js} +1 -1
  32. package/dist/docs/building-with-agents.html +3 -3
  33. package/dist/docs/concepts.html +3 -3
  34. package/dist/docs/deployment.html +3 -3
  35. package/dist/docs/evals.html +3 -3
  36. package/dist/docs/example-agents/approval-buddy.html +3 -3
  37. package/dist/docs/example-agents/benny.html +3 -3
  38. package/dist/docs/example-agents/bugbot.html +3 -3
  39. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  40. package/dist/docs/example-agents/codeowners-review.html +3 -3
  41. package/dist/docs/example-agents/concierge.html +3 -3
  42. package/dist/docs/example-agents/fsd.html +3 -3
  43. package/dist/docs/example-agents/index.html +3 -3
  44. package/dist/docs/example-agents/knowledge-base.html +3 -3
  45. package/dist/docs/example-agents/oncall.html +3 -3
  46. package/dist/docs/example-agents/security-reviewer.html +3 -3
  47. package/dist/docs/example-agents/slack-agent.html +3 -3
  48. package/dist/docs/example-agents/weather-agent.html +3 -3
  49. package/dist/docs/guides/agent-to-agent.html +3 -3
  50. package/dist/docs/guides/cloud-runtime.html +3 -3
  51. package/dist/docs/guides/convert-automation.html +3 -3
  52. package/dist/docs/guides/github.html +3 -3
  53. package/dist/docs/guides/human-in-the-loop.html +3 -3
  54. package/dist/docs/guides/mcp-oauth.html +3 -3
  55. package/dist/docs/guides/opentelemetry.html +3 -3
  56. package/dist/docs/guides/slack.html +3 -3
  57. package/dist/docs/guides/webhooks.html +3 -3
  58. package/dist/docs/hashmap.json +1 -1
  59. package/dist/docs/hillclimbing.html +3 -3
  60. package/dist/docs/index.html +3 -3
  61. package/dist/docs/quickstart.html +3 -3
  62. package/dist/docs/reference/agent-config.html +3 -3
  63. package/dist/docs/reference/artifacts.html +3 -3
  64. package/dist/docs/reference/channels.html +3 -3
  65. package/dist/docs/reference/cli.html +3 -3
  66. package/dist/docs/reference/connections.html +15 -5
  67. package/dist/docs/reference/hooks.html +3 -3
  68. package/dist/docs/reference/http-api.html +3 -3
  69. package/dist/docs/reference/instructions.html +3 -3
  70. package/dist/docs/reference/playground.html +3 -3
  71. package/dist/docs/reference/project-layout.html +3 -3
  72. package/dist/docs/reference/prompt.html +3 -3
  73. package/dist/docs/reference/schedules.html +3 -3
  74. package/dist/docs/reference/sessions.html +3 -3
  75. package/dist/docs/reference/skills.html +3 -3
  76. package/dist/docs/reference/subagents.html +3 -3
  77. package/dist/docs/reference/tools.html +5 -5
  78. package/dist/docs/scaffolding-agents.html +3 -3
  79. package/dist/docs/storage.html +3 -3
  80. package/dist/docs/templates/agentic-owners.html +3 -3
  81. package/dist/docs/templates/demo.html +3 -3
  82. package/dist/docs/templates/pr-autofixer.html +3 -3
  83. package/dist/docs/templates/security-reviewer.html +3 -3
  84. package/dist/docs/templates/triage.html +3 -3
  85. package/dist/docs/troubleshooting.html +3 -3
  86. package/dist/files-backends/agent-store-presigned-url.js +1 -1
  87. package/dist/index.d.ts +2 -2
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +1 -1
  90. package/dist/internal/advertise-tools.d.ts +74 -0
  91. package/dist/internal/advertise-tools.d.ts.map +1 -0
  92. package/dist/internal/advertise-tools.js +152 -0
  93. package/dist/internal/cli-deploy.d.ts.map +1 -1
  94. package/dist/internal/cli-deploy.js +11 -3
  95. package/dist/internal/cli-manifest.d.ts +19 -0
  96. package/dist/internal/cli-manifest.d.ts.map +1 -0
  97. package/dist/internal/cli-manifest.js +49 -0
  98. package/dist/internal/cli-slack.d.ts.map +1 -1
  99. package/dist/internal/cli-slack.js +9 -1
  100. package/dist/internal/cursor-agent-template.d.ts +9 -0
  101. package/dist/internal/cursor-agent-template.d.ts.map +1 -0
  102. package/dist/internal/cursor-agent-template.js +37 -0
  103. package/dist/internal/cursor-event-relay.d.ts +5 -1
  104. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  105. package/dist/internal/cursor-event-relay.js +5 -0
  106. package/dist/internal/deploy-client.d.ts +8 -0
  107. package/dist/internal/deploy-client.d.ts.map +1 -1
  108. package/dist/internal/deploy-client.js +21 -3
  109. package/dist/internal/deploy-manifest.d.ts +85 -0
  110. package/dist/internal/deploy-manifest.d.ts.map +1 -0
  111. package/dist/internal/deploy-manifest.js +106 -0
  112. package/dist/internal/deployment-scm-event-stream.d.ts +113 -0
  113. package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
  114. package/dist/internal/deployment-scm-event-stream.js +460 -0
  115. package/dist/internal/discovery.d.ts.map +1 -1
  116. package/dist/internal/discovery.js +69 -0
  117. package/dist/internal/init-project.d.ts.map +1 -1
  118. package/dist/internal/init-project.js +5 -17
  119. package/dist/internal/mcp-host.d.ts +18 -0
  120. package/dist/internal/mcp-host.d.ts.map +1 -1
  121. package/dist/internal/mcp-host.js +41 -5
  122. package/dist/internal/mcp-oauth.d.ts +0 -4
  123. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  124. package/dist/internal/mcp-oauth.js +0 -4
  125. package/dist/internal/sdk-runner.d.ts +1 -1
  126. package/dist/internal/sdk-runner.d.ts.map +1 -1
  127. package/dist/internal/sdk-runner.js +36 -13
  128. package/dist/internal/server.d.ts.map +1 -1
  129. package/dist/internal/server.js +20 -2
  130. package/dist/internal/session-engine.d.ts +66 -0
  131. package/dist/internal/session-engine.d.ts.map +1 -1
  132. package/dist/internal/session-engine.js +430 -110
  133. package/dist/internal/shallow-clone.d.ts +115 -0
  134. package/dist/internal/shallow-clone.d.ts.map +1 -0
  135. package/dist/internal/shallow-clone.js +292 -0
  136. package/dist/internal/slack-provision-client.d.ts +2 -0
  137. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  138. package/dist/internal/slack-provision-client.js +1 -0
  139. package/dist/internal/turn-lease-store.d.ts +44 -0
  140. package/dist/internal/turn-lease-store.d.ts.map +1 -0
  141. package/dist/internal/turn-lease-store.js +139 -0
  142. package/dist/internal/workspace.d.ts +12 -0
  143. package/dist/internal/workspace.d.ts.map +1 -1
  144. package/dist/internal/workspace.js +28 -0
  145. package/dist/playground/assets/index-CoL7xCJl.css +1 -0
  146. package/dist/playground/assets/index-yxs1heea.js +85 -0
  147. package/dist/playground/index.html +2 -2
  148. package/dist/storage-protocol.d.ts +122 -0
  149. package/dist/storage-protocol.d.ts.map +1 -0
  150. package/dist/storage-protocol.js +173 -0
  151. package/dist/storage.d.ts +2 -72
  152. package/dist/storage.d.ts.map +1 -1
  153. package/dist/storage.js +2 -82
  154. package/dist/types.d.ts +114 -4
  155. package/dist/types.d.ts.map +1 -1
  156. package/docs/reference/connections.md +80 -0
  157. package/docs/reference/tools.md +14 -0
  158. package/package.json +4 -2
  159. package/skills/github/SKILL.md +2 -1
  160. package/src/bin/agent-serve.ts +9 -0
  161. package/src/channels/github/github-channel.ts +18 -2
  162. package/src/channels/github/types.ts +2 -0
  163. package/src/channels/slack/channel-watch.ts +6 -3
  164. package/src/channels/slack/dispatch.ts +40 -3
  165. package/src/channels/slack/manifest.ts +1 -0
  166. package/src/channels/slack/nudge-store.ts +56 -0
  167. package/src/channels/slack/slack-channel.ts +3 -0
  168. package/src/channels/slack/types.ts +22 -5
  169. package/src/index.ts +5 -0
  170. package/src/internal/advertise-tools.ts +229 -0
  171. package/src/internal/cli-deploy.ts +22 -1
  172. package/src/internal/cli-manifest.ts +63 -0
  173. package/src/internal/cli-slack.ts +10 -1
  174. package/src/internal/cursor-agent-template.ts +46 -0
  175. package/src/internal/cursor-event-relay.ts +8 -1
  176. package/src/internal/deploy-client.ts +35 -6
  177. package/src/internal/deploy-manifest.ts +167 -0
  178. package/src/internal/deployment-scm-event-stream.ts +577 -0
  179. package/src/internal/discovery.ts +85 -0
  180. package/src/internal/init-project.ts +11 -20
  181. package/src/internal/mcp-host.ts +66 -9
  182. package/src/internal/mcp-oauth.ts +0 -4
  183. package/src/internal/sdk-runner.ts +50 -11
  184. package/src/internal/server.ts +21 -2
  185. package/src/internal/session-engine.ts +385 -19
  186. package/src/internal/shallow-clone.ts +407 -0
  187. package/src/internal/slack-provision-client.ts +3 -0
  188. package/src/internal/turn-lease-store.ts +147 -0
  189. package/src/internal/workspace.ts +27 -0
  190. package/src/storage-protocol.ts +247 -0
  191. package/src/storage.ts +16 -110
  192. package/src/types.ts +118 -4
  193. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
  194. package/dist/docs/assets/chunks/@localSearchIndexroot.BuymfBAo.js +0 -1
  195. package/dist/playground/assets/index-ClbuaLpm.css +0 -1
  196. package/dist/playground/assets/index-SqkR13k_.js +0 -85
@@ -70,7 +70,7 @@ import type {
70
70
  */
71
71
  type GitHubParsedContext = Omit<
72
72
  GitHubEventContext,
73
- "github" | "thread" | "host" | "artifacts"
73
+ "github" | "thread" | "host" | "artifacts" | "payload"
74
74
  >;
75
75
 
76
76
  const DEFAULT_PULL_REQUEST_ACTIONS: ReadonlySet<string> = new Set([
@@ -787,6 +787,7 @@ async function handlePullRequest(input: {
787
787
  const ctx = enrichEventContext({
788
788
  bindingConfig: input.bindingConfig,
789
789
  ctx: parsed.ctx,
790
+ payload: input.payload,
790
791
  bindingState: binding,
791
792
  installationId,
792
793
  host: input.args.host,
@@ -879,6 +880,7 @@ async function handleIssueComment(input: {
879
880
  const ctx = enrichEventContext({
880
881
  bindingConfig: input.bindingConfig,
881
882
  ctx: parsed.ctx,
883
+ payload: input.payload,
882
884
  bindingState: binding,
883
885
  installationId,
884
886
  host: input.args.host,
@@ -970,6 +972,7 @@ async function handleReviewComment(input: {
970
972
  const ctx = enrichEventContext({
971
973
  bindingConfig: input.bindingConfig,
972
974
  ctx: parsed.ctx,
975
+ payload: input.payload,
973
976
  bindingState: binding,
974
977
  installationId,
975
978
  host: input.args.host,
@@ -1050,6 +1053,7 @@ async function handleIssue(input: {
1050
1053
  const ctx = enrichEventContext({
1051
1054
  bindingConfig: input.bindingConfig,
1052
1055
  ctx: parsed.ctx,
1056
+ payload: input.payload,
1053
1057
  bindingState: binding,
1054
1058
  installationId,
1055
1059
  host: input.args.host,
@@ -1111,6 +1115,7 @@ async function handleCheckSuite(input: {
1111
1115
  enrichCiContext({
1112
1116
  bindingConfig: input.bindingConfig,
1113
1117
  ctx: parsed.unanchoredCtx,
1118
+ payload: input.payload,
1114
1119
  installationId,
1115
1120
  host: input.args.host,
1116
1121
  artifacts: input.args.artifacts,
@@ -1137,6 +1142,7 @@ async function handleCheckSuite(input: {
1137
1142
  const ctx = enrichEventContext({
1138
1143
  bindingConfig: input.bindingConfig,
1139
1144
  ctx: parsed.ctx,
1145
+ payload: input.payload,
1140
1146
  bindingState: binding,
1141
1147
  installationId,
1142
1148
  host: input.args.host,
@@ -1203,6 +1209,7 @@ async function handleCheckRun(input: {
1203
1209
  enrichCiContext({
1204
1210
  bindingConfig: input.bindingConfig,
1205
1211
  ctx: parsed.unanchoredCtx,
1212
+ payload: input.payload,
1206
1213
  installationId,
1207
1214
  host: input.args.host,
1208
1215
  artifacts: input.args.artifacts,
@@ -1229,6 +1236,7 @@ async function handleCheckRun(input: {
1229
1236
  const ctx = enrichEventContext({
1230
1237
  bindingConfig: input.bindingConfig,
1231
1238
  ctx: parsed.ctx,
1239
+ payload: input.payload,
1232
1240
  bindingState: binding,
1233
1241
  installationId,
1234
1242
  host: input.args.host,
@@ -1291,6 +1299,7 @@ async function handleWorkflowRun(input: {
1291
1299
  enrichCiContext({
1292
1300
  bindingConfig: input.bindingConfig,
1293
1301
  ctx: parsed.unanchoredCtx,
1302
+ payload: input.payload,
1294
1303
  installationId,
1295
1304
  host: input.args.host,
1296
1305
  artifacts: input.args.artifacts,
@@ -1317,6 +1326,7 @@ async function handleWorkflowRun(input: {
1317
1326
  const ctx = enrichEventContext({
1318
1327
  bindingConfig: input.bindingConfig,
1319
1328
  ctx: parsed.ctx,
1329
+ payload: input.payload,
1320
1330
  bindingState: binding,
1321
1331
  installationId,
1322
1332
  host: input.args.host,
@@ -1389,6 +1399,7 @@ async function handleStatus(input: {
1389
1399
  const ctx = enrichEventContext({
1390
1400
  bindingConfig: input.bindingConfig,
1391
1401
  ctx: parsed.ctx,
1402
+ payload: input.payload,
1392
1403
  bindingState: binding,
1393
1404
  installationId,
1394
1405
  host: input.args.host,
@@ -1463,6 +1474,7 @@ async function handleGenericEvent(input: {
1463
1474
  const ctx = enrichEventContext({
1464
1475
  bindingConfig: input.bindingConfig,
1465
1476
  ctx: parsed.ctx,
1477
+ payload: input.payload,
1466
1478
  bindingState: binding,
1467
1479
  installationId,
1468
1480
  host: input.args.host,
@@ -2353,10 +2365,11 @@ function escapeRegExp(value: string): string {
2353
2365
  */
2354
2366
  function enrichEventContext(input: {
2355
2367
  bindingConfig: GitHubBindingConfig;
2356
- ctx: Omit<GitHubEventContext, "github" | "thread" | "host" | "artifacts"> &
2368
+ ctx: GitHubParsedContext &
2357
2369
  Partial<Pick<GitHubEventContext, "github" | "thread">>;
2358
2370
  bindingState: GitHubBindingState;
2359
2371
  installationId?: number;
2372
+ payload: unknown;
2360
2373
  host: HostContext;
2361
2374
  artifacts: ArtifactsApi;
2362
2375
  }): GitHubEventContext {
@@ -2366,6 +2379,7 @@ function enrichEventContext(input: {
2366
2379
  });
2367
2380
  return {
2368
2381
  ...input.ctx,
2382
+ payload: input.payload,
2369
2383
  ...(input.installationId === undefined
2370
2384
  ? {}
2371
2385
  : { installationId: input.installationId }),
@@ -2384,6 +2398,7 @@ function enrichCiContext(input: {
2384
2398
  bindingConfig: GitHubBindingConfig;
2385
2399
  ctx: GitHubParsedContext;
2386
2400
  installationId?: number;
2401
+ payload: unknown;
2387
2402
  host: HostContext;
2388
2403
  artifacts: ArtifactsApi;
2389
2404
  }): GitHubEventContext {
@@ -2400,6 +2415,7 @@ function enrichCiContext(input: {
2400
2415
  ctx: input.ctx,
2401
2416
  bindingState: binding,
2402
2417
  installationId: input.installationId,
2418
+ payload: input.payload,
2403
2419
  host: input.host,
2404
2420
  artifacts: input.artifacts,
2405
2421
  });
@@ -320,6 +320,8 @@ export interface GitHubEventContext {
320
320
  sender?: GitHubUser;
321
321
  /** Installation id from the webhook payload, when present. */
322
322
  installationId?: number;
323
+ /** Verified webhook body for this delivery. */
324
+ payload: unknown;
323
325
  /** Authenticated GitHub helpers (`getOctokit`, `request`, `createStatus`). */
324
326
  github: GitHubHandle;
325
327
  /** Post / react on the conversation thread. */
@@ -21,10 +21,11 @@ import type { SlackChannelPostsOptions, SlackMessage } from "./types.js";
21
21
 
22
22
  const CHANNEL_ID_PATTERN = /^[CG][A-Z0-9]+$/;
23
23
 
24
- /** Allowlist over channel ids and `#names` (names resolve lazily). */
24
+ /** Allowlist over channel ids and `#names` (names resolve lazily), or `*`. */
25
25
  export class SlackChannelAllowlist {
26
26
  private readonly ids = new Set<string>();
27
27
  private readonly names = new Set<string>();
28
+ private all = false;
28
29
 
29
30
  constructor(entries: readonly string[]) {
30
31
  for (const raw of entries) {
@@ -32,7 +33,9 @@ export class SlackChannelAllowlist {
32
33
  if (entry === "") {
33
34
  continue;
34
35
  }
35
- if (entry.startsWith("#")) {
36
+ if (entry === "*") {
37
+ this.all = true;
38
+ } else if (entry.startsWith("#")) {
36
39
  this.names.add(entry.slice(1).toLowerCase());
37
40
  } else if (CHANNEL_ID_PATTERN.test(entry)) {
38
41
  this.ids.add(entry);
@@ -44,7 +47,7 @@ export class SlackChannelAllowlist {
44
47
  }
45
48
 
46
49
  matches(channelId: string): boolean {
47
- return this.ids.has(channelId);
50
+ return this.all || this.ids.has(channelId);
48
51
  }
49
52
 
50
53
  unresolvedNames(): string[] {
@@ -142,6 +142,13 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
142
142
  * dropped at parse time (loop safety).
143
143
  */
144
144
  botMentionGate?: SlackBotMentionGate;
145
+ /**
146
+ * When false, do not write `assistant.threads.setStatus` on a coalesced
147
+ * follow-up. Defaults to `true`. Channels that report progress their own
148
+ * way turn this off: the chip has no owner to clear it once the default
149
+ * `message.completed` handler is replaced, so it lingers past the turn.
150
+ */
151
+ followUpStatus?: boolean;
145
152
  /**
146
153
  * When false, skip the built-in `eval` directive. Defaults to true when
147
154
  * `host.evals` is present.
@@ -195,6 +202,8 @@ export async function dispatchInboundMessage(args: {
195
202
  log?: SlackLog;
196
203
  eventId?: string;
197
204
  evalCommandsEnabled?: boolean;
205
+ /** See {@link SlackInboundDispatchDeps.followUpStatus}. Defaults to `true`. */
206
+ followUpStatus?: boolean;
198
207
  /**
199
208
  * cursorAccount agent display name — stripped before parsing `eval …`
200
209
  * (`OncallBuddy eval list`).
@@ -477,8 +486,9 @@ export async function dispatchInboundMessage(args: {
477
486
  event_id: args.eventId,
478
487
  }
479
488
  );
480
- // Coalesce admits leave the active turn running; follow-ups drain into one
481
- // boundary turn when it finishes (no mid-turn tool/hook inject).
489
+ // Coalesce admits leave the active turn running: the follow-up is queued
490
+ // for the carrier to inject at the next tool boundary, and anything it
491
+ // does not reach in time drains into the boundary turn instead.
482
492
  if (session.coalesced === true) {
483
493
  log.info("session busy; coalescing follow-up", {
484
494
  kind,
@@ -487,8 +497,32 @@ export async function dispatchInboundMessage(args: {
487
497
  continuation,
488
498
  event_id: args.eventId,
489
499
  });
500
+ // Queue it for mid-turn injection too. The carrier drains this at the
501
+ // running turn's next tool boundary; whatever it does not reach in time
502
+ // still drains into the boundary turn, deduped by sourceTs.
503
+ try {
504
+ await args.host.slackNudges?.append({
505
+ continuationKey: continuation,
506
+ sourceTs: message.ts,
507
+ text: media.userText,
508
+ ...(message.author?.userName === undefined
509
+ ? {}
510
+ : { author: message.author.userName }),
511
+ });
512
+ } catch (nudgeError) {
513
+ log.warn("nudge append failed", {
514
+ error:
515
+ nudgeError instanceof Error
516
+ ? nudgeError.message
517
+ : String(nudgeError),
518
+ continuation,
519
+ event_id: args.eventId,
520
+ });
521
+ }
490
522
  try {
491
- await binding.thread.startTyping(FOLLOWUP_TYPING);
523
+ if (args.followUpStatus !== false) {
524
+ await binding.thread.startTyping(FOLLOWUP_TYPING);
525
+ }
492
526
  } catch (typingError) {
493
527
  log.error(
494
528
  `follow-up typing failed: ${
@@ -581,6 +615,7 @@ export function dispatchInboundFromEventBody(
581
615
  log,
582
616
  evalCommandsEnabled: deps.evalCommandsEnabled,
583
617
  cursorAgentName: deps.cursorAgentName,
618
+ followUpStatus: deps.followUpStatus,
584
619
  blockExternals: deps.blockExternals,
585
620
  ...(deps.hasContinuationSession === undefined
586
621
  ? {}
@@ -623,6 +658,7 @@ export function dispatchInboundFromEventBody(
623
658
  log,
624
659
  evalCommandsEnabled: deps.evalCommandsEnabled,
625
660
  cursorAgentName: deps.cursorAgentName,
661
+ followUpStatus: deps.followUpStatus,
626
662
  blockExternals: deps.blockExternals,
627
663
  ...(deps.hasContinuationSession === undefined
628
664
  ? {}
@@ -659,6 +695,7 @@ export function dispatchInboundFromEventBody(
659
695
  log,
660
696
  evalCommandsEnabled: false,
661
697
  cursorAgentName: deps.cursorAgentName,
698
+ followUpStatus: deps.followUpStatus,
662
699
  blockExternals: deps.blockExternals,
663
700
  ...(deps.hasContinuationSession === undefined
664
701
  ? {}
@@ -69,6 +69,7 @@ const BOT_SCOPES = [
69
69
  "channels:history",
70
70
  "chat:write",
71
71
  "files:read",
72
+ "files:write",
72
73
  "groups:history",
73
74
  "im:history",
74
75
  "im:read",
@@ -202,6 +202,24 @@ export class SlackNudgeStore {
202
202
  return (await this.listPending(continuationKey)).length > 0;
203
203
  }
204
204
 
205
+ /**
206
+ * How many follow-ups have reached the agent on this thread. Monotonic
207
+ * within a continuation, so a caller can spot a delivery by comparing two
208
+ * readings — unlike {@link hasUndelivered}, which races the carrier: both
209
+ * run at the same tool boundary, and a queued nudge drained before the
210
+ * first reading never looks pending at all.
211
+ */
212
+ async deliveredCount(continuationKey: string): Promise<number> {
213
+ if (continuationKey.trim() === "") {
214
+ return 0;
215
+ }
216
+ try {
217
+ return (await readdir(this.deliveredDir(continuationKey))).length;
218
+ } catch {
219
+ return 0;
220
+ }
221
+ }
222
+
205
223
  /**
206
224
  * Atomically move all pending nudges to delivered and return them.
207
225
  * Already-delivered source ts values are dropped (exactly-once).
@@ -238,6 +256,44 @@ export class SlackNudgeStore {
238
256
  return delivered;
239
257
  }
240
258
 
259
+ /**
260
+ * Settle a follow-up the caller is handling itself, so the carrier cannot
261
+ * inject it again. Used by the boundary drain: a follow-up the carrier did
262
+ * not reach becomes the next turn's prompt, and without this its pending
263
+ * record survives into that turn and gets injected on top of the ask it
264
+ * already is.
265
+ */
266
+ async markDelivered(
267
+ continuationKey: string,
268
+ sourceTs: string
269
+ ): Promise<void> {
270
+ const key = continuationKey.trim();
271
+ const ts = sourceTs.trim();
272
+ if (key === "" || ts === "") {
273
+ return;
274
+ }
275
+ const pending = await this.listPending(key);
276
+ const record = pending.find((item) => item.sourceTs === ts);
277
+ await mkdir(this.deliveredDir(key), { recursive: true });
278
+ const dest = this.deliveredPath(key, ts);
279
+ const tmp = `${dest}.${process.pid}.tmp`;
280
+ try {
281
+ await writeFile(
282
+ tmp,
283
+ `${JSON.stringify(record ?? { continuationKey: key, sourceTs: ts })}\n`,
284
+ "utf8"
285
+ );
286
+ await rename(tmp, dest);
287
+ } catch {
288
+ await rm(tmp, { force: true }).catch(() => {});
289
+ }
290
+ if (record !== undefined) {
291
+ await rm(join(this.pendingDir(key), `${record.id}.json`), {
292
+ force: true,
293
+ }).catch(() => {});
294
+ }
295
+ }
296
+
241
297
  async clear(continuationKey: string): Promise<void> {
242
298
  await rm(this.continuationDir(continuationKey), {
243
299
  recursive: true,
@@ -219,6 +219,7 @@ export function slackChannel<const Name extends string = string>(
219
219
  const mentionsEnabled = engagement.mentions !== false;
220
220
  const directMessagesEnabled = engagement.directMessages !== false;
221
221
  const evalCommandsEnabled = options.evalCommands !== false;
222
+ const followUpStatus = options.followUpStatus !== false;
222
223
  const channelPosts = engagement.channelPosts;
223
224
  if (channelPosts !== undefined) {
224
225
  const entries = channelPosts.allow.filter((entry) => entry.trim() !== "");
@@ -296,6 +297,7 @@ export function slackChannel<const Name extends string = string>(
296
297
  mentionsEnabled,
297
298
  directMessagesEnabled,
298
299
  evalCommandsEnabled,
300
+ followUpStatus,
299
301
  // cursorAccount: backend router already refuses externals.
300
302
  blockExternals: cursorAccount ? false : blockExternals,
301
303
  ...(args.hasContinuationSession === undefined
@@ -484,6 +486,7 @@ export function slackChannel<const Name extends string = string>(
484
486
  artifacts: args.artifacts,
485
487
  log: getLog(),
486
488
  evalCommandsEnabled,
489
+ followUpStatus,
487
490
  blockExternals,
488
491
  ...(args.hasContinuationSession === undefined
489
492
  ? {}
@@ -330,11 +330,18 @@ export type SlackBlockActionHandler = (
330
330
  */
331
331
  export interface SlackChannelPostsOptions {
332
332
  /**
333
- * Channels to watch: ids (`C…` / `G…`) or `#names`. Must be non-empty
334
- * (empty is a definition error). `#names` are resolved to ids via
335
- * `conversations.list` when the channel starts; the bot must be a member
336
- * of each watched channel to receive its messages. Socket Mode only —
337
- * `cursorAccount: true` rejects `engagement.channelPosts`.
333
+ * Channels to watch: ids (`C…` / `G…`), `#names`, or `"*"` for every
334
+ * channel the bot is in. Must be non-empty (empty is a definition error).
335
+ * `#names` are resolved to ids via `conversations.list` when the channel
336
+ * starts; the bot must be a member of each watched channel to receive its
337
+ * messages. Socket Mode only — `cursorAccount: true` rejects
338
+ * `engagement.channelPosts`.
339
+ *
340
+ * `"*"` suits agents whose handler decides per message (a thread the agent
341
+ * is already in, say) rather than per channel. Every delivered message then
342
+ * reaches {@link SlackChannelOptions.onChannelPost}, and each one costs a
343
+ * `users.info` external-sender check first unless
344
+ * {@link SlackAccessPolicy.blockExternals} is off.
338
345
  */
339
346
  allow: string[];
340
347
  /**
@@ -571,6 +578,16 @@ export interface SlackChannelOptions {
571
578
  * Defaults to `true`.
572
579
  */
573
580
  thinkingSteps?: boolean;
581
+ /**
582
+ * Write Slack's assistant status chip ("Saw your follow-up…") when a
583
+ * follow-up coalesces into a running turn. Defaults to `true`.
584
+ *
585
+ * Set to `false` on channels that report progress themselves: the chip is
586
+ * cleared by the default `message.completed` handler, so a channel that
587
+ * replaces that handler has nothing to take it down and it outlives the
588
+ * turn.
589
+ */
590
+ followUpStatus?: boolean;
574
591
  /**
575
592
  * Opt in to Slack delivery for human-in-the-loop tool approvals.
576
593
  *
package/src/index.ts CHANGED
@@ -132,6 +132,8 @@ export type {
132
132
  export { defineSchedule } from "./schedules.js";
133
133
  export { defineSkill } from "./skills.js";
134
134
  export type {
135
+ DurableSessionRecord,
136
+ DurableSessionSummary,
135
137
  StorageABTable,
136
138
  StorageCasTable,
137
139
  StorageConfig,
@@ -144,8 +146,11 @@ export type {
144
146
  } from "./storage.js";
145
147
  export {
146
148
  defineStorage,
149
+ isDurableSessionRecordKey,
150
+ parseDurableSessionRecord,
147
151
  STORAGE_KEY_ROOT,
148
152
  storageKeys,
153
+ summarizeDurableSessionRecord,
149
154
  } from "./storage.js";
150
155
  export { defineTool } from "./tools.js";
151
156
  export type * from "./types.js";
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Advertised connection tools (`defineConnection({ advertiseTools: true })`).
3
+ *
4
+ * The session engine calls {@link resolveAdvertisedTools} while building
5
+ * every local turn: for each advertising connection it lists the server's
6
+ * tools and synthesizes a named 1:1 passthrough server tool per entry —
7
+ * the server's own names, descriptions, and live schemas — merged into the
8
+ * authored `agent/tools/` catalog. Because it runs at turn-build time it
9
+ * covers every way a turn starts (dispatcher sends, follow-ups, reminder
10
+ * fires that call the engine directly) and survives process restarts,
11
+ * since nothing is parked in memory between turns.
12
+ *
13
+ * Connections with per-session `auth` are listed and called through a
14
+ * one-off client carrying that session's headers (the registry's cached
15
+ * per-name client would leak one session's identity into another);
16
+ * auth-less connections go through the shared `host.mcp` registry, which
17
+ * also covers serve-resolved transports (Cursor account bridges, peers).
18
+ *
19
+ * Everything here fails closed: a listing failure, an invalid tool name,
20
+ * or a name collision throws, and the engine fails the turn rather than
21
+ * running it with a silently shrunken toolset.
22
+ */
23
+
24
+ import type {
25
+ DiscoveredConnection,
26
+ DiscoveredTool,
27
+ HostMcpCallResult,
28
+ HostMcpToolInfo,
29
+ JsonSchemaObject,
30
+ ResolvedAgent,
31
+ SessionInfo,
32
+ ToolDefinition,
33
+ } from "../types.js";
34
+ import { brandDefinition } from "./brand.js";
35
+ import type { HostMcpOneOff } from "./mcp-host.js";
36
+
37
+ /**
38
+ * What the resolver needs from the host MCP registry: the shared cached
39
+ * clients for auth-less connections, and one-off clients (per-session
40
+ * headers, never cached) for auth'd ones. `McpHost` implements this.
41
+ */
42
+ export interface AdvertiseHostMcp {
43
+ listTools(connectionName: string): Promise<HostMcpToolInfo[]>;
44
+ callTool(
45
+ connectionName: string,
46
+ toolName: string,
47
+ args?: Record<string, unknown>
48
+ ): Promise<HostMcpCallResult>;
49
+ oneOff(connection: DiscoveredConnection): HostMcpOneOff;
50
+ }
51
+
52
+ /**
53
+ * The rule advertised tool names must match — the same shape discovery
54
+ * enforces for authored tool filenames (its `NAME_PATTERN`), since both
55
+ * become the tool name the model sees.
56
+ */
57
+ export const ADVERTISED_TOOL_NAME_PATTERN: RegExp =
58
+ /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
59
+
60
+ const ANY_OBJECT_SCHEMA: JsonSchemaObject = {
61
+ type: "object",
62
+ additionalProperties: true,
63
+ };
64
+
65
+ /** How one advertising connection is listed and called. */
66
+ export interface AdvertisedToolSource {
67
+ listTools(): Promise<HostMcpToolInfo[]>;
68
+ callTool(
69
+ toolName: string,
70
+ args: Record<string, unknown>
71
+ ): Promise<HostMcpCallResult>;
72
+ }
73
+
74
+ /**
75
+ * Resolve the advertised tools for one session. Returns `[]` when no
76
+ * connection advertises; throws (failing the turn) when a listing fails
77
+ * or a definition is invalid.
78
+ */
79
+ export async function resolveAdvertisedTools(args: {
80
+ agent: ResolvedAgent;
81
+ session: SessionInfo;
82
+ hostMcp: AdvertiseHostMcp;
83
+ /** Test seam; defaults to {@link sourceForConnection}. */
84
+ sourceFor?: (
85
+ connection: DiscoveredConnection,
86
+ session: SessionInfo,
87
+ hostMcp: AdvertiseHostMcp
88
+ ) => Promise<AdvertisedToolSource>;
89
+ }): Promise<DiscoveredTool[]> {
90
+ const advertising = args.agent.connections.filter(
91
+ (connection) => connection.advertiseTools === true
92
+ );
93
+ if (advertising.length === 0) {
94
+ return [];
95
+ }
96
+ const sourceFor = args.sourceFor ?? sourceForConnection;
97
+ const claimed = new Set(args.agent.tools.map((tool) => tool.name));
98
+ const resolved: DiscoveredTool[] = [];
99
+ for (const connection of advertising) {
100
+ const source = await sourceFor(connection, args.session, args.hostMcp);
101
+ let listed: HostMcpToolInfo[];
102
+ try {
103
+ listed = await source.listTools();
104
+ } catch (error) {
105
+ throw new Error(
106
+ `Failed to list tools on advertised connection "${connection.name}": ${describeError(error)}`,
107
+ { cause: error }
108
+ );
109
+ }
110
+ for (const info of listed) {
111
+ resolved.push(toDiscoveredTool(connection, info, claimed, source));
112
+ claimed.add(info.name);
113
+ }
114
+ }
115
+ return resolved;
116
+ }
117
+
118
+ /**
119
+ * The production source for one advertising connection: the shared
120
+ * `host.mcp` registry client for auth-less connections (which also covers
121
+ * serve-resolved transports — Cursor account bridges, peers), or a
122
+ * registry one-off with the session's headers substituted when the
123
+ * connection has per-session `auth`. The auth callback is evaluated once
124
+ * per turn, here; the one-off opens a fresh client per operation so one
125
+ * session's identity can never serve another's call.
126
+ */
127
+ export async function sourceForConnection(
128
+ connection: DiscoveredConnection,
129
+ session: SessionInfo,
130
+ hostMcp: AdvertiseHostMcp
131
+ ): Promise<AdvertisedToolSource> {
132
+ if (connection.auth === undefined) {
133
+ return {
134
+ listTools: () => hostMcp.listTools(connection.name),
135
+ callTool: (toolName, callArgs) =>
136
+ hostMcp.callTool(connection.name, toolName, callArgs),
137
+ };
138
+ }
139
+ const transport = connection.transport;
140
+ if (!("url" in transport)) {
141
+ // Discovery rejects auth on non-URL transports; guard for callers that
142
+ // bypass discovery (hand-built projects).
143
+ throw new Error(
144
+ `Connection "${connection.name}" has per-session auth but no URL transport.`
145
+ );
146
+ }
147
+ const evaluated = await connection.auth(session);
148
+ return hostMcp.oneOff({
149
+ ...connection,
150
+ transport: {
151
+ ...transport,
152
+ headers: { ...transport.headers, ...evaluated.headers },
153
+ },
154
+ });
155
+ }
156
+
157
+ function toDiscoveredTool(
158
+ connection: DiscoveredConnection,
159
+ info: HostMcpToolInfo,
160
+ claimed: ReadonlySet<string>,
161
+ source: AdvertisedToolSource
162
+ ): DiscoveredTool {
163
+ if (!ADVERTISED_TOOL_NAME_PATTERN.test(info.name)) {
164
+ throw new Error(
165
+ `Advertised connection "${connection.name}" serves tool name "${info.name}", which must match ${ADVERTISED_TOOL_NAME_PATTERN} (it becomes the tool name the model sees).`
166
+ );
167
+ }
168
+ if (claimed.has(info.name)) {
169
+ throw new Error(
170
+ `Advertised connection "${connection.name}" serves tool "${info.name}", which collides with an authored agent/tools/ tool or another advertised connection.`
171
+ );
172
+ }
173
+ const description =
174
+ info.description === undefined || info.description.trim() === ""
175
+ ? `${info.name} — MCP tool on the "${connection.name}" connection, forwarded 1:1.`
176
+ : info.description;
177
+ const inputSchema = info.inputSchema ?? ANY_OBJECT_SCHEMA;
178
+ const definition: ToolDefinition = brandDefinition("tool", {
179
+ description,
180
+ inputSchema,
181
+ execute: async (input: Record<string, unknown>) =>
182
+ formatMcpToolResult(
183
+ connection.name,
184
+ info.name,
185
+ await source.callTool(info.name, input ?? {})
186
+ ),
187
+ } as unknown as ToolDefinition);
188
+ return {
189
+ name: info.name,
190
+ description,
191
+ inputSchema,
192
+ execution: "server",
193
+ needsApproval: false,
194
+ definition,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Render an MCP call result for the model: text parts joined, non-text
200
+ * parts noted by type, `isError` surfaced as a thrown error so the turn
201
+ * records a tool error instead of error-shaped prose.
202
+ */
203
+ export function formatMcpToolResult(
204
+ connectionName: string,
205
+ toolName: string,
206
+ result: HostMcpCallResult
207
+ ): string {
208
+ const parts = result.content.map((item) =>
209
+ item.type === "text" && typeof item.text === "string"
210
+ ? item.text
211
+ : `[${item.type} content omitted]`
212
+ );
213
+ const text = parts.join("\n").trim();
214
+ if (result.isError === true) {
215
+ throw new Error(
216
+ text === "" ? `${connectionName} tool ${toolName} failed` : text
217
+ );
218
+ }
219
+ if (text !== "") {
220
+ return text;
221
+ }
222
+ return result.structuredContent !== undefined
223
+ ? JSON.stringify(result.structuredContent, null, 2)
224
+ : "(empty result)";
225
+ }
226
+
227
+ function describeError(error: unknown): string {
228
+ return error instanceof Error ? error.message : String(error);
229
+ }