@cursor/july 0.1.62 → 0.1.73

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 (286) hide show
  1. package/AGENTS.md +4 -5
  2. package/README.md +23 -11
  3. package/dist/bin/agent-serve.js +11 -2
  4. package/dist/channels/github/api.d.ts +2 -0
  5. package/dist/channels/github/api.d.ts.map +1 -1
  6. package/dist/channels/github/api.js +2 -1
  7. package/dist/channels/github/binding.d.ts +7 -0
  8. package/dist/channels/github/binding.d.ts.map +1 -1
  9. package/dist/channels/github/binding.js +4 -0
  10. package/dist/channels/github/checks.d.ts +99 -0
  11. package/dist/channels/github/checks.d.ts.map +1 -0
  12. package/dist/channels/github/checks.js +108 -0
  13. package/dist/channels/github/cursor-account.d.ts +4 -4
  14. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  15. package/dist/channels/github/cursor-account.js +7 -5
  16. package/dist/channels/github/index.d.ts +1 -0
  17. package/dist/channels/github/index.d.ts.map +1 -1
  18. package/dist/channels/github/index.js +1 -0
  19. package/dist/channels/github/types.d.ts +8 -0
  20. package/dist/channels/github/types.d.ts.map +1 -1
  21. package/dist/channels/slack/channel-watch.d.ts +2 -1
  22. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  23. package/dist/channels/slack/channel-watch.js +7 -3
  24. package/dist/channels/slack/dispatch.d.ts +9 -0
  25. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  26. package/dist/channels/slack/dispatch.js +41 -21
  27. package/dist/channels/slack/manifest.d.ts +2 -2
  28. package/dist/channels/slack/manifest.d.ts.map +1 -1
  29. package/dist/channels/slack/manifest.js +14 -11
  30. package/dist/channels/slack/nudge-store.d.ts +16 -0
  31. package/dist/channels/slack/nudge-store.d.ts.map +1 -1
  32. package/dist/channels/slack/nudge-store.js +53 -0
  33. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  34. package/dist/channels/slack/slack-channel.js +4 -1
  35. package/dist/channels/slack/types.d.ts +24 -7
  36. package/dist/channels/slack/types.d.ts.map +1 -1
  37. package/dist/connections.d.ts +17 -5
  38. package/dist/connections.d.ts.map +1 -1
  39. package/dist/connections.js +17 -5
  40. package/dist/docs/404.html +2 -2
  41. package/dist/docs/ab.html +3 -3
  42. package/dist/docs/assets/{app.B-biogkN.js → app.CdbcFD8T.js} +1 -1
  43. package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +1 -0
  44. package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.D_NKImM7.js} +1 -1
  45. package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.CMvz7cu9.js} +2 -2
  46. package/dist/docs/assets/{concepts.md.CqOsxbMU.js → concepts.md.BXAm6G-C.js} +1 -1
  47. package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.js → guides_mcp-oauth.md.CzEB6RaG.js} +1 -1
  48. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.js → reference_agent-config.md.BZnEKfEc.js} +3 -4
  49. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.lean.js → reference_agent-config.md.BZnEKfEc.lean.js} +1 -1
  50. package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Cu3N-S3Q.js} +26 -9
  51. package/dist/docs/assets/reference_connections.md.Cu3N-S3Q.lean.js +1 -0
  52. package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.js → reference_sessions.md.B0DdlM-K.js} +1 -1
  53. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.DTg_kEsx.js} +2 -2
  54. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.DTg_kEsx.lean.js} +1 -1
  55. package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +1 -0
  56. package/dist/docs/building-with-agents.html +3 -3
  57. package/dist/docs/concepts.html +5 -5
  58. package/dist/docs/deployment.html +3 -3
  59. package/dist/docs/evals.html +3 -3
  60. package/dist/docs/example-agents/approval-buddy.html +3 -3
  61. package/dist/docs/example-agents/benny.html +3 -3
  62. package/dist/docs/example-agents/bugbot.html +3 -3
  63. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  64. package/dist/docs/example-agents/codeowners-review.html +3 -3
  65. package/dist/docs/example-agents/concierge.html +3 -3
  66. package/dist/docs/example-agents/fsd.html +3 -3
  67. package/dist/docs/example-agents/index.html +3 -3
  68. package/dist/docs/example-agents/knowledge-base.html +3 -3
  69. package/dist/docs/example-agents/oncall.html +3 -3
  70. package/dist/docs/example-agents/security-reviewer.html +3 -3
  71. package/dist/docs/example-agents/slack-agent.html +3 -3
  72. package/dist/docs/example-agents/weather-agent.html +3 -3
  73. package/dist/docs/guides/agent-to-agent.html +3 -3
  74. package/dist/docs/guides/cloud-runtime.html +3 -3
  75. package/dist/docs/guides/convert-automation.html +3 -3
  76. package/dist/docs/guides/github.html +3 -3
  77. package/dist/docs/guides/human-in-the-loop.html +3 -3
  78. package/dist/docs/guides/mcp-oauth.html +5 -5
  79. package/dist/docs/guides/opentelemetry.html +3 -3
  80. package/dist/docs/guides/slack.html +3 -3
  81. package/dist/docs/guides/webhooks.html +3 -3
  82. package/dist/docs/hashmap.json +1 -1
  83. package/dist/docs/hillclimbing.html +3 -3
  84. package/dist/docs/index.html +3 -3
  85. package/dist/docs/quickstart.html +3 -3
  86. package/dist/docs/reference/agent-config.html +5 -6
  87. package/dist/docs/reference/artifacts.html +3 -3
  88. package/dist/docs/reference/channels.html +3 -3
  89. package/dist/docs/reference/cli.html +3 -3
  90. package/dist/docs/reference/connections.html +28 -11
  91. package/dist/docs/reference/hooks.html +3 -3
  92. package/dist/docs/reference/http-api.html +3 -3
  93. package/dist/docs/reference/instructions.html +3 -3
  94. package/dist/docs/reference/playground.html +3 -3
  95. package/dist/docs/reference/project-layout.html +3 -3
  96. package/dist/docs/reference/prompt.html +3 -3
  97. package/dist/docs/reference/schedules.html +3 -3
  98. package/dist/docs/reference/sessions.html +5 -5
  99. package/dist/docs/reference/skills.html +3 -3
  100. package/dist/docs/reference/subagents.html +3 -3
  101. package/dist/docs/reference/tools.html +5 -5
  102. package/dist/docs/scaffolding-agents.html +3 -3
  103. package/dist/docs/storage.html +3 -3
  104. package/dist/docs/templates/agentic-owners.html +3 -3
  105. package/dist/docs/templates/demo.html +3 -3
  106. package/dist/docs/templates/pr-autofixer.html +3 -3
  107. package/dist/docs/templates/security-reviewer.html +3 -3
  108. package/dist/docs/templates/triage.html +3 -3
  109. package/dist/docs/troubleshooting.html +5 -5
  110. package/dist/files-backends/agent-store-presigned-url.js +1 -1
  111. package/dist/index.d.ts +2 -2
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +1 -1
  114. package/dist/internal/advertise-tools.d.ts +79 -0
  115. package/dist/internal/advertise-tools.d.ts.map +1 -0
  116. package/dist/internal/advertise-tools.js +185 -0
  117. package/dist/internal/cli-deploy.d.ts.map +1 -1
  118. package/dist/internal/cli-deploy.js +7 -1
  119. package/dist/internal/cli-manifest.d.ts +19 -0
  120. package/dist/internal/cli-manifest.d.ts.map +1 -0
  121. package/dist/internal/cli-manifest.js +49 -0
  122. package/dist/internal/cli-slack.d.ts.map +1 -1
  123. package/dist/internal/cli-slack.js +9 -1
  124. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -1
  125. package/dist/internal/convert-automation/convert-workflow.js +2 -10
  126. package/dist/internal/cursor/account-mcp.d.ts +29 -3
  127. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  128. package/dist/internal/cursor/account-mcp.js +147 -67
  129. package/dist/internal/cursor/backend-client.d.ts +11 -6
  130. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  131. package/dist/internal/cursor/backend-client.js +32 -10
  132. package/dist/internal/cursor-account-mcp-auth.d.ts +11 -0
  133. package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -1
  134. package/dist/internal/cursor-account-mcp-auth.js +69 -0
  135. package/dist/internal/cursor-agent-template.d.ts +9 -0
  136. package/dist/internal/cursor-agent-template.d.ts.map +1 -0
  137. package/dist/internal/cursor-agent-template.js +37 -0
  138. package/dist/internal/deploy-client.d.ts +1 -0
  139. package/dist/internal/deploy-client.d.ts.map +1 -1
  140. package/dist/internal/deploy-client.js +18 -2
  141. package/dist/internal/deploy-manifest.d.ts +76 -0
  142. package/dist/internal/deploy-manifest.d.ts.map +1 -0
  143. package/dist/internal/deploy-manifest.js +117 -0
  144. package/dist/internal/deployment-scm-event-stream.d.ts +116 -0
  145. package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
  146. package/dist/internal/deployment-scm-event-stream.js +474 -0
  147. package/dist/internal/discovery.d.ts.map +1 -1
  148. package/dist/internal/discovery.js +111 -10
  149. package/dist/internal/init-project.d.ts.map +1 -1
  150. package/dist/internal/init-project.js +12 -123
  151. package/dist/internal/init-scaffold.d.ts +8 -0
  152. package/dist/internal/init-scaffold.d.ts.map +1 -0
  153. package/dist/internal/init-scaffold.js +168 -0
  154. package/dist/internal/inline-stdio-mcp.d.ts +2 -0
  155. package/dist/internal/inline-stdio-mcp.d.ts.map +1 -1
  156. package/dist/internal/inline-stdio-mcp.js +10 -8
  157. package/dist/internal/json-schema.d.ts +7 -0
  158. package/dist/internal/json-schema.d.ts.map +1 -1
  159. package/dist/internal/json-schema.js +18 -3
  160. package/dist/internal/mcp-endpoint.js +7 -7
  161. package/dist/internal/mcp-host.d.ts +18 -0
  162. package/dist/internal/mcp-host.d.ts.map +1 -1
  163. package/dist/internal/mcp-host.js +43 -5
  164. package/dist/internal/mcp-oauth.d.ts +22 -0
  165. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  166. package/dist/internal/mcp-oauth.js +40 -1
  167. package/dist/internal/package-inline-stdio.d.ts.map +1 -1
  168. package/dist/internal/package-inline-stdio.js +5 -5
  169. package/dist/internal/review-comments.d.ts +97 -0
  170. package/dist/internal/review-comments.d.ts.map +1 -1
  171. package/dist/internal/review-comments.js +151 -0
  172. package/dist/internal/sanitize-mcp-tool-name.d.ts +20 -0
  173. package/dist/internal/sanitize-mcp-tool-name.d.ts.map +1 -0
  174. package/dist/internal/sanitize-mcp-tool-name.js +30 -0
  175. package/dist/internal/sdk-runner.d.ts +1 -1
  176. package/dist/internal/sdk-runner.d.ts.map +1 -1
  177. package/dist/internal/sdk-runner.js +36 -13
  178. package/dist/internal/server.d.ts.map +1 -1
  179. package/dist/internal/server.js +18 -0
  180. package/dist/internal/session-engine.d.ts +66 -0
  181. package/dist/internal/session-engine.d.ts.map +1 -1
  182. package/dist/internal/session-engine.js +428 -110
  183. package/dist/internal/shallow-clone.d.ts +170 -0
  184. package/dist/internal/shallow-clone.d.ts.map +1 -0
  185. package/dist/internal/shallow-clone.js +412 -0
  186. package/dist/internal/slack-provision-client.d.ts +2 -0
  187. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  188. package/dist/internal/slack-provision-client.js +1 -0
  189. package/dist/internal/terminal-style.d.ts +18 -0
  190. package/dist/internal/terminal-style.d.ts.map +1 -1
  191. package/dist/internal/terminal-style.js +46 -1
  192. package/dist/internal/turn-lease-store.d.ts +44 -0
  193. package/dist/internal/turn-lease-store.d.ts.map +1 -0
  194. package/dist/internal/turn-lease-store.js +139 -0
  195. package/dist/internal/workspace.d.ts +14 -0
  196. package/dist/internal/workspace.d.ts.map +1 -1
  197. package/dist/internal/workspace.js +33 -0
  198. package/dist/playground/assets/index-77uhu5Kr.css +1 -0
  199. package/dist/playground/assets/index-D4buox4m.js +85 -0
  200. package/dist/playground/index.html +2 -2
  201. package/dist/storage-protocol.d.ts +122 -0
  202. package/dist/storage-protocol.d.ts.map +1 -0
  203. package/dist/storage-protocol.js +173 -0
  204. package/dist/storage.d.ts +2 -72
  205. package/dist/storage.d.ts.map +1 -1
  206. package/dist/storage.js +2 -82
  207. package/dist/tools.d.ts +7 -0
  208. package/dist/tools.d.ts.map +1 -1
  209. package/dist/types.d.ts +141 -6
  210. package/dist/types.d.ts.map +1 -1
  211. package/docs/concepts.md +4 -4
  212. package/docs/guides/mcp-oauth.md +3 -1
  213. package/docs/reference/agent-config.md +11 -12
  214. package/docs/reference/connections.md +108 -10
  215. package/docs/reference/sessions.md +2 -2
  216. package/docs/reference/tools.md +14 -0
  217. package/docs/troubleshooting.md +3 -2
  218. package/package.json +7 -2
  219. package/skills/create-agent/SKILL.md +12 -9
  220. package/skills/debug/SKILL.md +2 -1
  221. package/skills/framework-map/SKILL.md +6 -6
  222. package/skills/mcp-auth/SKILL.md +8 -6
  223. package/src/bin/agent-serve.ts +12 -2
  224. package/src/channels/github/api.ts +6 -1
  225. package/src/channels/github/binding.ts +21 -0
  226. package/src/channels/github/checks.ts +179 -0
  227. package/src/channels/github/cursor-account.ts +9 -6
  228. package/src/channels/github/index.ts +1 -0
  229. package/src/channels/github/types.ts +8 -0
  230. package/src/channels/slack/channel-watch.ts +6 -3
  231. package/src/channels/slack/dispatch.ts +40 -3
  232. package/src/channels/slack/manifest.ts +9 -10
  233. package/src/channels/slack/nudge-store.ts +56 -0
  234. package/src/channels/slack/slack-channel.ts +3 -0
  235. package/src/channels/slack/types.ts +24 -7
  236. package/src/connections.ts +17 -5
  237. package/src/index.ts +5 -0
  238. package/src/internal/advertise-tools.ts +278 -0
  239. package/src/internal/cli-deploy.ts +11 -1
  240. package/src/internal/cli-manifest.ts +63 -0
  241. package/src/internal/cli-slack.ts +10 -1
  242. package/src/internal/convert-automation/convert-workflow.ts +2 -11
  243. package/src/internal/cursor/account-mcp.ts +208 -73
  244. package/src/internal/cursor/backend-client.ts +45 -11
  245. package/src/internal/cursor-account-mcp-auth.ts +83 -0
  246. package/src/internal/cursor-agent-template.ts +46 -0
  247. package/src/internal/deploy-client.ts +25 -6
  248. package/src/internal/deploy-manifest.ts +161 -0
  249. package/src/internal/deployment-scm-event-stream.ts +593 -0
  250. package/src/internal/discovery.ts +132 -3
  251. package/src/internal/init-project.ts +18 -153
  252. package/src/internal/init-scaffold.ts +176 -0
  253. package/src/internal/inline-stdio-mcp.ts +13 -0
  254. package/src/internal/json-schema.ts +24 -3
  255. package/src/internal/mcp-endpoint.ts +6 -0
  256. package/src/internal/mcp-host.ts +70 -9
  257. package/src/internal/mcp-oauth.ts +55 -1
  258. package/src/internal/package-inline-stdio.ts +5 -1
  259. package/src/internal/review-comments.ts +245 -0
  260. package/src/internal/sanitize-mcp-tool-name.ts +33 -0
  261. package/src/internal/sdk-runner.ts +50 -11
  262. package/src/internal/server.ts +19 -0
  263. package/src/internal/session-engine.ts +392 -19
  264. package/src/internal/shallow-clone.ts +595 -0
  265. package/src/internal/slack-provision-client.ts +3 -0
  266. package/src/internal/terminal-style.ts +67 -1
  267. package/src/internal/turn-lease-store.ts +147 -0
  268. package/src/internal/workspace.ts +33 -0
  269. package/src/storage-protocol.ts +247 -0
  270. package/src/storage.ts +16 -110
  271. package/src/tools.ts +7 -0
  272. package/src/types.ts +149 -7
  273. package/templates/agentic-owners/agent/agent.ts +3 -0
  274. package/templates/demo/agent/agent.ts +3 -0
  275. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
  276. package/templates/security-reviewer/agent/agent.ts +3 -0
  277. package/templates/triage/agent/agent.ts +3 -0
  278. package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
  279. package/dist/docs/assets/reference_connections.md.CSQiNzre.lean.js +0 -1
  280. package/dist/docs/assets/troubleshooting.md.CuVztCKs.js +0 -1
  281. package/dist/playground/assets/index-B4rcOgeP.css +0 -1
  282. package/dist/playground/assets/index-DXcY42Jv.js +0 -85
  283. /package/dist/docs/assets/{concepts.md.CqOsxbMU.lean.js → concepts.md.BXAm6G-C.lean.js} +0 -0
  284. /package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.lean.js → guides_mcp-oauth.md.CzEB6RaG.lean.js} +0 -0
  285. /package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.lean.js → reference_sessions.md.B0DdlM-K.lean.js} +0 -0
  286. /package/dist/docs/assets/{troubleshooting.md.CuVztCKs.lean.js → troubleshooting.md.DQ-hYuZT.lean.js} +0 -0
@@ -18,6 +18,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
18
18
  import { mkdir, rm, writeFile } from "node:fs/promises";
19
19
  import { dirname, join } from "node:path";
20
20
  import { mergeABInstructions, resolveABMaxPlaygroundSessions, resolveABSplit, } from "../ab.js";
21
+ import { SLACK_CHANNEL_ID } from "../channels/slack/constants.js";
22
+ import { materializeSlackNudgeCarrier } from "../channels/slack/nudge-carrier.js";
21
23
  import { SlackAskDedupeStore, SlackNudgeStore, } from "../channels/slack/nudge-store.js";
22
24
  import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
23
25
  import { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
@@ -27,6 +29,7 @@ import { DEFAULT_MODEL, DIRECT_TOOL_CALL_CHANNEL_ID, isHostOauthConnectionTransp
27
29
  import { ABCollector } from "./ab-collector.js";
28
30
  import { foldABStream } from "./ab-fold.js";
29
31
  import { buildABSnapshot } from "./ab-snapshot.js";
32
+ import { resolveAdvertisedTools } from "./advertise-tools.js";
30
33
  import { ApprovalGate } from "./approval-gate.js";
31
34
  import { ArtifactsStore } from "./artifacts-store.js";
32
35
  import { SessionAuthError, samePrincipal } from "./auth.js";
@@ -54,7 +57,8 @@ import { StorageCoordinator } from "./storage-coordinator.js";
54
57
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
55
58
  import { estimateTurnCostFromApi, roundCostUsd, } from "./turn-cost.js";
56
59
  import { TurnGovernor } from "./turn-governor.js";
57
- import { buildAgentsMdContent, buildCloudMemorySection, isNestedInGitRepo, materializeWorkspace, SERVER_TOOLS_MCP_SERVER_NAME, writeWorkspaceFiles, } from "./workspace.js";
60
+ import { TurnLeaseStore } from "./turn-lease-store.js";
61
+ import { buildAgentsMdContent, buildCloudMemorySection, ensureOwnGitRoot, isNestedInGitRepo, materializeWorkspace, SERVER_TOOLS_MCP_SERVER_NAME, writeWorkspaceFiles, } from "./workspace.js";
58
62
  export const HTTP_CHANNEL_ID = "http";
59
63
  export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
60
64
  export class SessionBusyError extends Error {
@@ -121,6 +125,12 @@ export class SessionEngine {
121
125
  this.activeTurns = new Map();
122
126
  /** Context used by the cloud turn's HTTP MCP callbacks. */
123
127
  this.activeToolContexts = new Map();
128
+ /**
129
+ * Delivered-nudge count per session at the last tool boundary. The carrier
130
+ * drains out-of-process, so a growing count is how the engine learns a
131
+ * follow-up reached the agent.
132
+ */
133
+ this.deliveredNudges = new Map();
124
134
  /** Aborts the in-flight turn per session so a follow-up can preempt it. */
125
135
  this.turnAborts = new Map();
126
136
  /** Serializes turn admission per session (latest follow-up wins). */
@@ -162,6 +172,7 @@ export class SessionEngine {
162
172
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
163
173
  this.nudgeStore = new SlackNudgeStore(join(options.stateRoot, "slack-nudges"));
164
174
  this.askDedupe = new SlackAskDedupeStore(join(options.stateRoot, "slack-asks"));
175
+ this.turnLeases = new TurnLeaseStore(join(options.stateRoot, "turn-leases"));
165
176
  this.logs = new EventLogStore((sessionId) => this.sessions.eventFilePath(sessionId), (sessionId, error) => this.logger(`[agent-sdk] failed to persist events for ${sessionId}: ${describeError(error)}`));
166
177
  // Framework session state (records, event chunks, continuations) is
167
178
  // mirrored to whatever this selects; see framework-storage-selection.ts
@@ -405,6 +416,102 @@ export class SessionEngine {
405
416
  return record;
406
417
  });
407
418
  }
419
+ /**
420
+ * Install the Slack nudge carrier into a local session's harness cwd, so a
421
+ * follow-up that arrives mid-turn reaches the running agent at its next tool
422
+ * boundary instead of waiting for the boundary turn.
423
+ *
424
+ * Slack-only and local-only: the carrier is a Cursor hook in the workspace,
425
+ * and a cloud VM has neither the workspace nor the nudge store.
426
+ *
427
+ * The harness only reads these hooks when the workspace is its own git root;
428
+ * nested in an outer repo it reads that repo's instead and the carrier never
429
+ * runs, leaving follow-ups to drain at the turn boundary as before.
430
+ */
431
+ materializeNudgeCarrier(record, harnessCwd) {
432
+ return __awaiter(this, void 0, void 0, function* () {
433
+ if (record.channelId !== SLACK_CHANNEL_ID ||
434
+ record.continuationKey === null ||
435
+ record.continuationKey === "" ||
436
+ // Borrowed: a PR worktree, or a checkout shared by every session. Hooks
437
+ // written there dirty a tree this session did not create, and a shared
438
+ // cwd holds one continuation pointer for all of them — the carrier would
439
+ // inject one thread's follow-ups into another thread's turn. Nothing is
440
+ // lost: a borrowed checkout is its own git root, so the harness reads its
441
+ // hooks rather than these, and follow-ups drain at the turn boundary.
442
+ this.hasBorrowedWorkspace(record)) {
443
+ return;
444
+ }
445
+ try {
446
+ yield materializeSlackNudgeCarrier({
447
+ harnessCwd,
448
+ nudgeRootDir: join(this.stateRoot, "slack-nudges"),
449
+ continuationKey: record.continuationKey,
450
+ });
451
+ }
452
+ catch (error) {
453
+ // Best-effort: without the carrier, follow-ups still drain at the turn
454
+ // boundary — one turn later, never lost.
455
+ this.logger(`[session] slack nudge carrier not installed: ${describeError(error)}`);
456
+ }
457
+ });
458
+ }
459
+ /**
460
+ * Report a mid-turn follow-up reaching the agent, as `nudge.delivered`.
461
+ *
462
+ * The workspace carrier drains `pending/` into `delivered/` out of process,
463
+ * so the engine watches the delivered count grow rather than the turn's own
464
+ * events. Counting rather than edge-detecting on "still queued" matters:
465
+ * the carrier runs at the same tool boundary this check does, so a nudge
466
+ * queued and drained between two readings is never seen as pending.
467
+ *
468
+ * Channels need this to move a live progress message below the follow-up —
469
+ * Slack cannot reorder messages.
470
+ */
471
+ emitDeliveredNudges(record, emit) {
472
+ return __awaiter(this, void 0, void 0, function* () {
473
+ const delivered = yield this.readDeliveredNudges(record);
474
+ if (delivered === undefined) {
475
+ return;
476
+ }
477
+ const previous = this.deliveredNudges.get(record.sessionId);
478
+ this.deliveredNudges.set(record.sessionId, delivered);
479
+ if (previous !== undefined && delivered > previous) {
480
+ emit({ type: "nudge.delivered", data: {} });
481
+ }
482
+ });
483
+ }
484
+ /**
485
+ * Record the thread's delivered count before the turn runs. Earlier turns
486
+ * leave their own records behind, so only growth from here is this turn's.
487
+ */
488
+ baselineDeliveredNudges(record) {
489
+ return __awaiter(this, void 0, void 0, function* () {
490
+ const delivered = yield this.readDeliveredNudges(record);
491
+ if (delivered !== undefined) {
492
+ this.deliveredNudges.set(record.sessionId, delivered);
493
+ }
494
+ });
495
+ }
496
+ readDeliveredNudges(record) {
497
+ return __awaiter(this, void 0, void 0, function* () {
498
+ const continuationKey = record.continuationKey;
499
+ // Same gate as the carrier: only Slack queues follow-ups mid-turn, and
500
+ // this runs on every tool boundary of every turn.
501
+ if (record.channelId !== SLACK_CHANNEL_ID ||
502
+ continuationKey === null ||
503
+ continuationKey === "") {
504
+ return undefined;
505
+ }
506
+ try {
507
+ return yield this.nudgeStore.deliveredCount(continuationKey);
508
+ }
509
+ catch (_a) {
510
+ // A store read must never break the turn it is observing.
511
+ return undefined;
512
+ }
513
+ });
514
+ }
408
515
  slackNudgeHostApi() {
409
516
  return {
410
517
  append: (input) => this.nudgeStore.append(input),
@@ -697,6 +804,61 @@ export class SessionEngine {
697
804
  clearCoalesced(sessionId) {
698
805
  this.coalesceQueues.delete(sessionId);
699
806
  }
807
+ /**
808
+ * Drop queued follow-ups the nudge carrier already carried into the turn
809
+ * that just finished — the contract {@link SendMessageOptions.coalesceSourceTs}
810
+ * describes. Re-asking them would answer the same message twice.
811
+ *
812
+ * Items with no `sourceTs` cannot be matched and are kept.
813
+ */
814
+ withoutNudgedFollowups(sessionId, queued) {
815
+ return __awaiter(this, void 0, void 0, function* () {
816
+ const record = yield this.sessions.get(sessionId);
817
+ const continuationKey = record === null || record === void 0 ? void 0 : record.continuationKey;
818
+ if (continuationKey === undefined || continuationKey === null) {
819
+ return [...queued];
820
+ }
821
+ const kept = [];
822
+ for (const item of queued) {
823
+ if (item.sourceTs === undefined) {
824
+ kept.push(item);
825
+ continue;
826
+ }
827
+ try {
828
+ if (!(yield this.nudgeStore.isDelivered(continuationKey, item.sourceTs))) {
829
+ kept.push(item);
830
+ }
831
+ }
832
+ catch (_a) {
833
+ // A store read failure must not swallow the ask.
834
+ kept.push(item);
835
+ }
836
+ }
837
+ return kept;
838
+ });
839
+ }
840
+ /** Mark drained follow-ups delivered so the next turn's carrier skips them. */
841
+ settleDrainedFollowups(sessionId, items) {
842
+ return __awaiter(this, void 0, void 0, function* () {
843
+ const record = yield this.sessions.get(sessionId);
844
+ const continuationKey = record === null || record === void 0 ? void 0 : record.continuationKey;
845
+ if (continuationKey === undefined || continuationKey === null) {
846
+ return;
847
+ }
848
+ for (const item of items) {
849
+ if (item.sourceTs === undefined) {
850
+ continue;
851
+ }
852
+ try {
853
+ yield this.nudgeStore.markDelivered(continuationKey, item.sourceTs);
854
+ }
855
+ catch (error) {
856
+ // Worst case the carrier re-injects it; the ask still gets answered.
857
+ this.logger(`[session] nudge markDelivered failed for ${sessionId}: ${describeError(error)}`);
858
+ }
859
+ }
860
+ });
861
+ }
700
862
  formatCoalescedFollowups(items) {
701
863
  if (items.length === 1) {
702
864
  return items[0].message;
@@ -722,8 +884,17 @@ export class SessionEngine {
722
884
  return;
723
885
  }
724
886
  this.coalesceQueues.delete(sessionId);
725
- const message = this.formatCoalescedFollowups(queued);
726
- this.logger(`[session] coalesce drain starting follow-up (${queued.length}) ${sessionId}`);
887
+ const pending = yield this.withoutNudgedFollowups(sessionId, queued);
888
+ if (pending.length === 0) {
889
+ this.logger(`[session] coalesce drain skipped; all follow-ups already nudged ${sessionId}`);
890
+ return;
891
+ }
892
+ // These become the next turn's prompt, so settle them in the nudge store
893
+ // first: a surviving pending record would be injected by that turn's own
894
+ // carrier, re-asking what the turn is already about.
895
+ yield this.settleDrainedFollowups(sessionId, pending);
896
+ const message = this.formatCoalescedFollowups(pending);
897
+ this.logger(`[session] coalesce drain starting follow-up (${pending.length}) ${sessionId}`);
727
898
  yield this.startTurn(sessionId, message, {
728
899
  admission: "preempt",
729
900
  retainCoalesced: true,
@@ -741,6 +912,14 @@ export class SessionEngine {
741
912
  this.logger(`[session] coalesce enqueue ${sessionId}`);
742
913
  return true;
743
914
  }
915
+ // Another server owns this session, so coalesce even a preempt. No local
916
+ // enqueue: `drainCoalescedFollowups` reads only this process's queue, so
917
+ // the shared nudge store is the carrier. Not redundant with `acquire`
918
+ // below — this returns before preempt's `clearCoalesced` drops the queue.
919
+ if (yield this.turnLeases.runningElsewhere(sessionId)) {
920
+ this.logger(`[session] coalesce; another server owns ${sessionId}`);
921
+ return true;
922
+ }
744
923
  // Interrupt any turn in flight and wait for it to settle so the SDK run is
745
924
  // terminal before this turn's send (the follow-up also force-expires as a
746
925
  // belt-and-suspenders — see buildTurnRequest). Drain-resume skips interrupt:
@@ -754,7 +933,20 @@ export class SessionEngine {
754
933
  if (admission === "preempt" && options.retainCoalesced !== true) {
755
934
  this.clearCoalesced(sessionId);
756
935
  }
936
+ // A neighbour won the race; same outcome as the branch above.
937
+ const lease = yield this.turnLeases.acquire(sessionId, () => {
938
+ var _a;
939
+ // Stalled past the stale window and a successor took over. Stand down
940
+ // rather than answer the thread alongside it.
941
+ this.logger(`[session] lease lost mid-turn; aborting ${sessionId}`);
942
+ (_a = this.turnAborts.get(sessionId)) === null || _a === void 0 ? void 0 : _a.abort();
943
+ });
944
+ if (lease === undefined) {
945
+ this.logger(`[session] lease lost during admit ${sessionId}`);
946
+ return true;
947
+ }
757
948
  this.pendingTurns.add(sessionId);
949
+ let lifecycleInstalled = false;
758
950
  try {
759
951
  let record = yield this.updateRecord(sessionId, (current) => (Object.assign(Object.assign({}, current), { turnCount: current.turnCount + 1 })));
760
952
  // Arms must be on the record before tools/instructions see SessionInfo.
@@ -807,12 +999,21 @@ export class SessionEngine {
807
999
  }
808
1000
  }
809
1001
  }
1002
+ // The drain may have nested a successor turn, which took its own
1003
+ // reference; the lease outlives this release until that one ends too.
1004
+ yield lease.release();
810
1005
  return outcome;
811
1006
  }))();
812
1007
  this.activeTurns.set(sessionId, lifecycle);
1008
+ lifecycleInstalled = true;
813
1009
  }
814
1010
  finally {
815
1011
  this.pendingTurns.delete(sessionId);
1012
+ if (!lifecycleInstalled) {
1013
+ // Nothing will run the release above, and the session would sit behind
1014
+ // this lease until it went stale.
1015
+ yield lease.release();
1016
+ }
816
1017
  }
817
1018
  return false;
818
1019
  });
@@ -955,9 +1156,15 @@ export class SessionEngine {
955
1156
  }
956
1157
  const startedAt = Date.now();
957
1158
  try {
958
- const outcome = yield this.runner.prewarm(Object.assign(Object.assign(Object.assign({ workspaceDir, runnerStateDir: join(this.stateRoot, "runner"), sandbox: ((_b = agent.local) === null || _b === void 0 ? void 0 : _b.sandbox) === true }, (agent.allowedTools === undefined
1159
+ const outcome = yield this.runner.prewarm(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ workspaceDir, runnerStateDir: join(this.stateRoot, "runner"), sandbox: ((_b = agent.local) === null || _b === void 0 ? void 0 : _b.sandbox) === true }, (agent.allowedTools === undefined
959
1160
  ? {}
960
- : { tools: agent.allowedTools })), { mcpServers: this.buildMcpServers("local"), subagents: buildRunnerSubagents(agent) }), (agent.model === undefined ? {} : { model: agent.model })));
1161
+ : { tools: agent.allowedTools })), {
1162
+ // No session yet, so auth'd connections are omitted rather than
1163
+ // warmed without an identity; the per-send flag still has to match
1164
+ // the turns' requests or the warmed executor is one no turn asks for.
1165
+ mcpServers: this.buildMcpServers("local") }), (this.hasAttachedAuthedConnections()
1166
+ ? { perSendMcpServers: true }
1167
+ : {})), { subagents: buildRunnerSubagents(agent) }), (agent.model === undefined ? {} : { model: agent.model })));
961
1168
  // Report what happened rather than that the call returned: a runner
962
1169
  // that cannot warm resolves cleanly, and logging success there would
963
1170
  // tell an operator the workspace is hot when nothing touched it.
@@ -1029,6 +1236,18 @@ export class SessionEngine {
1029
1236
  seed: isFirstTurn,
1030
1237
  stateRoot: this.stateRoot,
1031
1238
  });
1239
+ // Must precede the carrier: the harness resolves rules, skills,
1240
+ // `mcp.json` and hooks from its git root, so a workspace nested in an
1241
+ // outer repo would load that repo's hooks instead of the ones written
1242
+ // just below. Only for directories this session owns — a borrowed
1243
+ // checkout or worktree keeps its own root.
1244
+ if (!this.hasBorrowedWorkspace(record)) {
1245
+ yield ensureOwnGitRoot(harnessCwd);
1246
+ }
1247
+ yield this.materializeNudgeCarrier(record, harnessCwd);
1248
+ // Baseline before the agent can run, so a follow-up that arrives and
1249
+ // drains before the first tool boundary still counts as an increase.
1250
+ yield this.baselineDeliveredNudges(record);
1032
1251
  const pendingFiles = record.pendingWorkspaceFiles;
1033
1252
  const pendingImages = record.pendingImages;
1034
1253
  if (pendingFiles !== undefined &&
@@ -1052,7 +1271,7 @@ export class SessionEngine {
1052
1271
  const instructionsInvisibleToHarness = isFirstTurn &&
1053
1272
  (this.hasBorrowedWorkspace(record) ||
1054
1273
  (yield isNestedInGitRepo(harnessCwd)));
1055
- const request = this.buildTurnRequest(record, agent, turnId, message, isFirstTurn, instructionsInvisibleToHarness, emit, (update) => __awaiter(this, void 0, void 0, function* () {
1274
+ const request = yield this.buildTurnRequest(record, agent, turnId, message, isFirstTurn, instructionsInvisibleToHarness, emit, (update) => __awaiter(this, void 0, void 0, function* () {
1056
1275
  mapper.handleUpdate(update);
1057
1276
  // Yield so the HTTP NDJSON writer can flush tool start/result
1058
1277
  // events while sibling parallel tools are still executing.
@@ -1061,6 +1280,9 @@ export class SessionEngine {
1061
1280
  // Nested subagent progress (newer SDKs); string check keeps
1062
1281
  // typecheck green against older published `@cursor/sdk` pins.
1063
1282
  update.type === "tool-call-delta") {
1283
+ // Same boundary the carrier hook runs on, so a drain is visible
1284
+ // by the time this fires.
1285
+ yield this.emitDeliveredNudges(record, emit);
1064
1286
  yield yieldEventLoop();
1065
1287
  }
1066
1288
  }), signal, pendingImages);
@@ -1080,6 +1302,9 @@ export class SessionEngine {
1080
1302
  permit.release();
1081
1303
  this.approvals.cancelSession(sessionId, "deny");
1082
1304
  yield this.clearPendingApprovals(sessionId);
1305
+ // Anything still queued belongs to the next turn's boundary drain, not
1306
+ // to this turn's edge detection.
1307
+ this.deliveredNudges.delete(sessionId);
1083
1308
  }
1084
1309
  }
1085
1310
  mapper.finish();
@@ -1143,88 +1368,106 @@ export class SessionEngine {
1143
1368
  });
1144
1369
  }
1145
1370
  buildTurnRequest(record, agent, turnId, prompt, isFirstTurn, instructionsInvisibleToHarness, emit, onUpdate, signal, images) {
1146
- var _a, _b, _c;
1147
- // Per-session cloud attach (channel/tool handoff) forces cloud runtime
1148
- // even when the agent defaults to local for chat server tools.
1149
- const runtime = resolveSessionRuntime({
1150
- agentRuntime: agent.runtime,
1151
- cloudOverride: record.cloudOverride,
1152
- });
1153
- // Fail closed: the SDK cannot enforce the built-in tool allowlist on
1154
- // cloud agents yet, and a restriction that silently does not apply is
1155
- // worse than a refused turn. Cloud-runtime agents already fail
1156
- // discovery; this guards hybrid agents' per-send cloud sessions.
1157
- if (runtime === "cloud" && agent.allowedTools !== undefined) {
1158
- throw new Error(`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`);
1159
- }
1160
- const mergedCloud = runtime === "cloud"
1161
- ? mergeCloudOptions({
1162
- base: agent.cloud,
1163
- override: record.cloudOverride,
1164
- })
1165
- : undefined;
1166
- let turnPrompt = prompt;
1167
- if (isFirstTurn) {
1168
- const contextBlock = buildSessionContextBlock({
1169
- sessionId: record.sessionId,
1170
- channelId: record.channelId,
1171
- channelKind: (_b = (_a = this.channelsById.get(record.channelId)) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.kind,
1172
- environment: isDevMode() ? "local dev server" : "hosted deployment",
1371
+ return __awaiter(this, void 0, void 0, function* () {
1372
+ var _a, _b, _c;
1373
+ // Per-session cloud attach (channel/tool handoff) forces cloud runtime
1374
+ // even when the agent defaults to local for chat server tools.
1375
+ const runtime = resolveSessionRuntime({
1376
+ agentRuntime: agent.runtime,
1377
+ cloudOverride: record.cloudOverride,
1173
1378
  });
1174
- turnPrompt = `${contextBlock}\n\n${prompt}`;
1175
- }
1176
- if (isFirstTurn) {
1177
- if (runtime === "cloud") {
1178
- // Cloud VMs have no materialized workspace: ship the full context,
1179
- // including agent tool script bodies. On hosted deployments the
1180
- // shared memory journal is on the store mount — say where. When the
1181
- // turn carries the session-bound server-tool MCP endpoint, the
1182
- // preamble catalogs server tools under that MCP server name *before*
1183
- // the agent-tool "write the script if missing" recipe, so a model
1184
- // told to "use <server tool>" calls MCP instead of inventing a .sh.
1185
- const preamble = buildAgentsMdContent(agent, {
1186
- includeScripts: true,
1187
- serverToolsMcpName: this.cloudServerToolsConnection === undefined
1188
- ? undefined
1189
- : SERVER_TOOLS_MCP_SERVER_NAME,
1379
+ // Fail closed: the SDK cannot enforce the built-in tool allowlist on
1380
+ // cloud agents yet, and a restriction that silently does not apply is
1381
+ // worse than a refused turn. Cloud-runtime agents already fail
1382
+ // discovery; this guards hybrid agents' per-send cloud sessions.
1383
+ if (runtime === "cloud" && agent.allowedTools !== undefined) {
1384
+ throw new Error(`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`);
1385
+ }
1386
+ // Same fail-closed posture for per-session connection features:
1387
+ // advertised tools are server tools (in-process, local turns only) and
1388
+ // per-session auth headers cannot reach a cloud VM's MCP config. A turn
1389
+ // that silently runs without its per-session tools or identity is worse
1390
+ // than a refused one. Cloud-runtime agents already fail discovery; this
1391
+ // guards hybrid agents' per-send cloud sessions.
1392
+ const perSessionConnection = agent.connections.find((connection) => connection.advertiseTools === true || connection.auth !== undefined);
1393
+ if (runtime === "cloud" && perSessionConnection !== undefined) {
1394
+ throw new Error(`Agent "${agent.name}" connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on local turns. Refusing the cloud session rather than running it without the connection's tools or identity.`);
1395
+ }
1396
+ const mergedCloud = runtime === "cloud"
1397
+ ? mergeCloudOptions({
1398
+ base: agent.cloud,
1399
+ override: record.cloudOverride,
1400
+ })
1401
+ : undefined;
1402
+ let turnPrompt = prompt;
1403
+ if (isFirstTurn) {
1404
+ const contextBlock = buildSessionContextBlock({
1405
+ sessionId: record.sessionId,
1406
+ channelId: record.channelId,
1407
+ channelKind: (_b = (_a = this.channelsById.get(record.channelId)) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.kind,
1408
+ environment: isDevMode() ? "local dev server" : "hosted deployment",
1190
1409
  });
1191
- const memorySection = isCursorHostedFilesAvailable()
1192
- ? `\n\n${buildCloudMemorySection(this.project.name)}`
1193
- : "";
1194
- turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
1195
- }
1196
- else if (instructionsInvisibleToHarness) {
1197
- // Local workspace whose AGENTS.md the harness will not load. With a
1198
- // borrowed harness cwd, agent tool scripts live only in the durable
1199
- // scaffold dir embed bodies like cloud. Otherwise the catalog can
1200
- // reference on-disk paths under the harness cwd.
1201
- const includeScripts = this.hasBorrowedWorkspace(record);
1202
- const preamble = buildAgentsMdContent(agent, { includeScripts });
1203
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
1204
- }
1205
- }
1206
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ sessionId: record.sessionId }, (record.sdkAgentId === undefined
1207
- ? {}
1208
- : { sdkAgentId: record.sdkAgentId })), { workspaceDir: this.harnessCwd(record), sandbox: ((_c = this.project.agent.local) === null || _c === void 0 ? void 0 : _c.sandbox) === true }), (runtime === "local" && agent.allowedTools !== undefined
1209
- ? { tools: agent.allowedTools }
1210
- : {})), { runnerStateDir: join(this.stateRoot, "runner"), prompt: turnPrompt, isFirstTurn }), (images !== undefined && images.length > 0 ? { images } : {})), (agent.model === undefined ? {} : { model: agent.model })), { runtime }), (mergedCloud === undefined ? {} : { cloud: mergedCloud })), { customTools: runtime === "local" ? this.buildCustomTools(record, turnId, emit) : {}, mcpServers: this.buildMcpServers(runtime, record.sessionId), subagents: buildRunnerSubagents(agent), onUpdate,
1211
- signal,
1212
- // Local follow-ups expire any lingering active run so a preempted turn
1213
- // (or one orphaned by a crashed process) never blocks this send.
1214
- forceExpireActiveRun: runtime === "local" && !isFirstTurn, onSdkAgentId: (sdkAgentId) => __awaiter(this, void 0, void 0, function* () {
1215
- if (record.sdkAgentId === sdkAgentId) {
1216
- return;
1410
+ turnPrompt = `${contextBlock}\n\n${prompt}`;
1411
+ }
1412
+ if (isFirstTurn) {
1413
+ if (runtime === "cloud") {
1414
+ // Cloud VMs have no materialized workspace: ship the full context,
1415
+ // including agent tool script bodies. On hosted deployments the
1416
+ // shared memory journal is on the store mount say where. When the
1417
+ // turn carries the session-bound server-tool MCP endpoint, the
1418
+ // preamble catalogs server tools under that MCP server name *before*
1419
+ // the agent-tool "write the script if missing" recipe, so a model
1420
+ // told to "use <server tool>" calls MCP instead of inventing a .sh.
1421
+ const preamble = buildAgentsMdContent(agent, {
1422
+ includeScripts: true,
1423
+ serverToolsMcpName: this.cloudServerToolsConnection === undefined
1424
+ ? undefined
1425
+ : SERVER_TOOLS_MCP_SERVER_NAME,
1426
+ });
1427
+ const memorySection = isCursorHostedFilesAvailable()
1428
+ ? `\n\n${buildCloudMemorySection(this.project.name)}`
1429
+ : "";
1430
+ turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
1217
1431
  }
1218
- yield this.updateRecord(record.sessionId, (current) => (Object.assign(Object.assign({}, current), { sdkAgentId })));
1219
- record.sdkAgentId = sdkAgentId;
1220
- yield this.appendEvent(record.sessionId, {
1221
- type: "agent.bound",
1222
- turnId,
1223
- data: Object.assign({ sdkAgentId }, (sdkAgentId.startsWith("bc-")
1224
- ? { url: `https://cursor.com/agents/${sdkAgentId}` }
1225
- : {})),
1226
- });
1227
- }) });
1432
+ else if (instructionsInvisibleToHarness) {
1433
+ // Local workspace whose AGENTS.md the harness will not load. With a
1434
+ // borrowed harness cwd, agent tool scripts live only in the durable
1435
+ // scaffold dir — embed bodies like cloud. Otherwise the catalog can
1436
+ // reference on-disk paths under the harness cwd.
1437
+ const includeScripts = this.hasBorrowedWorkspace(record);
1438
+ const preamble = buildAgentsMdContent(agent, { includeScripts });
1439
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
1440
+ }
1441
+ }
1442
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ sessionId: record.sessionId }, (record.sdkAgentId === undefined
1443
+ ? {}
1444
+ : { sdkAgentId: record.sdkAgentId })), { workspaceDir: this.harnessCwd(record), sandbox: ((_c = this.project.agent.local) === null || _c === void 0 ? void 0 : _c.sandbox) === true }), (runtime === "local" && agent.allowedTools !== undefined
1445
+ ? { tools: agent.allowedTools }
1446
+ : {})), { runnerStateDir: join(this.stateRoot, "runner"), prompt: turnPrompt, isFirstTurn }), (images !== undefined && images.length > 0 ? { images } : {})), (agent.model === undefined ? {} : { model: agent.model })), { runtime }), (mergedCloud === undefined ? {} : { cloud: mergedCloud })), { customTools: runtime === "local"
1447
+ ? yield this.buildCustomTools(record, agent, turnId, emit)
1448
+ : {}, mcpServers: this.buildMcpServers(runtime, record.sessionId, yield this.evaluateConnectionAuth(record)) }), (runtime === "local" && this.hasAttachedAuthedConnections()
1449
+ ? { perSendMcpServers: true }
1450
+ : {})), { subagents: buildRunnerSubagents(agent), onUpdate,
1451
+ signal,
1452
+ // Local follow-ups expire any lingering active run so a preempted turn
1453
+ // (or one orphaned by a crashed process) never blocks this send.
1454
+ forceExpireActiveRun: runtime === "local" && !isFirstTurn, onRequestId: (requestId) => {
1455
+ emit({ type: "turn.bound", data: { requestId } });
1456
+ }, onSdkAgentId: (sdkAgentId) => __awaiter(this, void 0, void 0, function* () {
1457
+ if (record.sdkAgentId === sdkAgentId) {
1458
+ return;
1459
+ }
1460
+ yield this.updateRecord(record.sessionId, (current) => (Object.assign(Object.assign({}, current), { sdkAgentId })));
1461
+ record.sdkAgentId = sdkAgentId;
1462
+ yield this.appendEvent(record.sessionId, {
1463
+ type: "agent.bound",
1464
+ turnId,
1465
+ data: Object.assign({ sdkAgentId }, (sdkAgentId.startsWith("bc-")
1466
+ ? { url: `https://cursor.com/agents/${sdkAgentId}` }
1467
+ : {})),
1468
+ });
1469
+ }) });
1470
+ });
1228
1471
  }
1229
1472
  /**
1230
1473
  * Connection names a cloud turn can only reach through the host connection
@@ -1288,9 +1531,12 @@ export class SessionEngine {
1288
1531
  * mount as host connection bridges over the tool MCP endpoint instead of
1289
1532
  * being omitted or failing the turn closed.
1290
1533
  */
1291
- buildMcpServers(runtime, sessionId) {
1534
+ buildMcpServers(runtime, sessionId, sessionAuth) {
1535
+ // Advertised connections surface as named server tools instead of an
1536
+ // attached MCP server (each tool appears exactly once), so they are
1537
+ // excluded here alongside hostOnly.
1292
1538
  const hostOnlyNames = new Set(this.project.agent.connections
1293
- .filter((connection) => connection.hostOnly === true)
1539
+ .filter((connection) => connection.hostOnly === true || connection.advertiseTools === true)
1294
1540
  .map((connection) => connection.name));
1295
1541
  const bridgedNames = new Set(runtime === "cloud" &&
1296
1542
  sessionId !== undefined &&
@@ -1327,6 +1573,20 @@ export class SessionEngine {
1327
1573
  }
1328
1574
  continue;
1329
1575
  }
1576
+ // Per-session auth: merge the evaluated headers over the static
1577
+ // ones. With no evaluation in hand (prewarm has no session) the
1578
+ // connection is omitted rather than attached without its identity.
1579
+ if (connection.auth !== undefined) {
1580
+ const evaluated = sessionAuth === null || sessionAuth === void 0 ? void 0 : sessionAuth.get(connection.name);
1581
+ if (evaluated === undefined) {
1582
+ continue;
1583
+ }
1584
+ const transport = connection.transport;
1585
+ if ("url" in transport) {
1586
+ servers[connection.name] = Object.assign(Object.assign({}, transport), { headers: Object.assign(Object.assign({}, transport.headers), evaluated) });
1587
+ }
1588
+ continue;
1589
+ }
1330
1590
  servers[connection.name] = connection.transport;
1331
1591
  }
1332
1592
  for (const resolved of this.resolvedConnections) {
@@ -1365,32 +1625,88 @@ export class SessionEngine {
1365
1625
  }
1366
1626
  return servers;
1367
1627
  }
1368
- buildCustomTools(record, turnId, emit) {
1369
- const tools = {};
1370
- for (const tool of this.project.agent.tools) {
1371
- if (tool.execution !== "server") {
1372
- continue;
1628
+ /**
1629
+ * True when a connection with per-session `auth` attaches to the model
1630
+ * (not `hostOnly`, not advertised). These turns set
1631
+ * {@link RunnerTurnRequest.perSendMcpServers} so the evaluated headers
1632
+ * ride every send instead of being pinned on the cached agent handle.
1633
+ */
1634
+ hasAttachedAuthedConnections() {
1635
+ return this.project.agent.connections.some((connection) => connection.auth !== undefined &&
1636
+ connection.hostOnly !== true &&
1637
+ connection.advertiseTools !== true);
1638
+ }
1639
+ /**
1640
+ * Evaluate per-session connection auth (`defineConnection({ auth })`)
1641
+ * for connections that attach to the model this turn. Advertised
1642
+ * connections evaluate their own auth inside the tool synthesis path. A
1643
+ * callback that throws fails the turn — a turn never silently runs
1644
+ * without the connection's identity.
1645
+ */
1646
+ evaluateConnectionAuth(record) {
1647
+ return __awaiter(this, void 0, void 0, function* () {
1648
+ const authed = this.project.agent.connections.filter((connection) => connection.auth !== undefined &&
1649
+ connection.hostOnly !== true &&
1650
+ connection.advertiseTools !== true);
1651
+ if (authed.length === 0) {
1652
+ return undefined;
1373
1653
  }
1374
- const execute = tool.definition.execute;
1375
- if (typeof execute !== "function") {
1376
- continue;
1654
+ const session = this.sessionInfo(record);
1655
+ const evaluated = new Map();
1656
+ for (const connection of authed) {
1657
+ const auth = connection.auth;
1658
+ if (auth === undefined) {
1659
+ continue;
1660
+ }
1661
+ try {
1662
+ evaluated.set(connection.name, (yield auth(session)).headers);
1663
+ }
1664
+ catch (error) {
1665
+ throw new Error(`Connection "${connection.name}" auth failed: ${describeError(error)}`, { cause: error });
1666
+ }
1377
1667
  }
1378
- tools[tool.name] = {
1379
- description: tool.description,
1380
- inputSchema: tool.inputSchema,
1381
- execute: (args, context) => __awaiter(this, void 0, void 0, function* () {
1382
- return this.executeServerTool({
1383
- tool,
1384
- input: args,
1385
- toolCallId: context.toolCallId,
1386
- record,
1387
- turnId,
1388
- emit,
1389
- });
1390
- }),
1668
+ return evaluated;
1669
+ });
1670
+ }
1671
+ buildCustomTools(record, agent, turnId, emit) {
1672
+ return __awaiter(this, void 0, void 0, function* () {
1673
+ const tools = {};
1674
+ const register = (tool) => {
1675
+ tools[tool.name] = Object.assign(Object.assign({ description: tool.description, inputSchema: tool.inputSchema }, (tool.outputSchema === undefined
1676
+ ? {}
1677
+ : { outputSchema: tool.outputSchema })), { execute: (args, context) => __awaiter(this, void 0, void 0, function* () {
1678
+ return this.executeServerTool({
1679
+ tool,
1680
+ input: args,
1681
+ toolCallId: context.toolCallId,
1682
+ record,
1683
+ turnId,
1684
+ emit,
1685
+ });
1686
+ }) });
1391
1687
  };
1392
- }
1393
- return tools;
1688
+ for (const tool of agent.tools) {
1689
+ if (tool.execution !== "server") {
1690
+ continue;
1691
+ }
1692
+ if (typeof tool.definition.execute !== "function") {
1693
+ continue;
1694
+ }
1695
+ register(tool);
1696
+ }
1697
+ // Advertised connections (`defineConnection({ advertiseTools })`):
1698
+ // named passthroughs resolved fresh on every turn — reminder fires and
1699
+ // post-restart follow-ups included — and a listing failure throws,
1700
+ // failing the turn closed.
1701
+ for (const tool of yield resolveAdvertisedTools({
1702
+ agent,
1703
+ session: this.sessionInfo(record),
1704
+ hostMcp: this.mcpHost,
1705
+ })) {
1706
+ register(tool);
1707
+ }
1708
+ return tools;
1709
+ });
1394
1710
  }
1395
1711
  /**
1396
1712
  * True while `sessionId` has a turn in flight. The host connection bridge
@@ -2577,6 +2893,8 @@ export class SessionEngine {
2577
2893
  yield Promise.allSettled([...this.backgroundWork]);
2578
2894
  yield Promise.allSettled([...this.dispatchChains.values()]);
2579
2895
  yield Promise.allSettled([...this.updateChains.values()]);
2896
+ // After the drain, so a successor need not wait out the heartbeat TTL.
2897
+ yield this.turnLeases.releaseAll().catch(() => { });
2580
2898
  yield this.logs.flushAll();
2581
2899
  yield (_c = this.storage) === null || _c === void 0 ? void 0 : _c.close();
2582
2900
  yield this.mcpHost.close();