@cursor/july 0.1.9 → 0.1.11

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 (200) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/bin/agent-serve.js +26 -3
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +2 -1
  5. package/dist/channels/github/types.d.ts +4 -0
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  8. package/dist/channels/slack/cursor-account.js +1 -1
  9. package/dist/channels/slack/dispatch.d.ts +13 -0
  10. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  11. package/dist/channels/slack/dispatch.js +36 -11
  12. package/dist/channels/slack/eval-directive.d.ts +63 -0
  13. package/dist/channels/slack/eval-directive.d.ts.map +1 -0
  14. package/dist/channels/slack/eval-directive.js +283 -0
  15. package/dist/channels/slack/index.d.ts +1 -0
  16. package/dist/channels/slack/index.d.ts.map +1 -1
  17. package/dist/channels/slack/index.js +1 -0
  18. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  19. package/dist/channels/slack/slack-channel.js +4 -2
  20. package/dist/channels/slack/types.d.ts +7 -0
  21. package/dist/channels/slack/types.d.ts.map +1 -1
  22. package/dist/connections.d.ts +10 -5
  23. package/dist/connections.d.ts.map +1 -1
  24. package/dist/connections.js +10 -5
  25. package/dist/docs/404.html +2 -2
  26. package/dist/docs/ab.html +4 -4
  27. package/dist/docs/assets/{app.Ciosf24M.js → app.BcXUGca7.js} +1 -1
  28. package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
  29. package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
  30. package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.BnvulOX2.js} +2 -2
  31. package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
  32. package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
  33. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
  34. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
  35. package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
  36. package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
  37. package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.B74NrVzb.js} +5 -4
  38. package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
  39. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.CsSr3rDz.js} +9 -4
  40. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
  41. package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
  42. package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
  43. package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
  44. package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
  45. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
  46. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
  47. package/dist/docs/building-with-agents.html +4 -4
  48. package/dist/docs/concepts.html +4 -4
  49. package/dist/docs/deployment.html +7 -7
  50. package/dist/docs/evals.html +4 -4
  51. package/dist/docs/example-agents/approval-buddy.html +4 -4
  52. package/dist/docs/example-agents/benny.html +4 -4
  53. package/dist/docs/example-agents/bugbot.html +4 -4
  54. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  55. package/dist/docs/example-agents/codeowners-review.html +4 -4
  56. package/dist/docs/example-agents/concierge.html +4 -4
  57. package/dist/docs/example-agents/fsd.html +4 -4
  58. package/dist/docs/example-agents/index.html +4 -4
  59. package/dist/docs/example-agents/knowledge-base.html +4 -4
  60. package/dist/docs/example-agents/oncall.html +4 -4
  61. package/dist/docs/example-agents/security-reviewer.html +4 -4
  62. package/dist/docs/example-agents/slack-agent.html +4 -4
  63. package/dist/docs/example-agents/weather-agent.html +4 -4
  64. package/dist/docs/guides/agent-to-agent.html +4 -4
  65. package/dist/docs/guides/cloud-runtime.html +4 -4
  66. package/dist/docs/guides/github.html +4 -4
  67. package/dist/docs/guides/human-in-the-loop.html +4 -4
  68. package/dist/docs/guides/mcp-oauth.html +53 -0
  69. package/dist/docs/guides/slack.html +4 -4
  70. package/dist/docs/guides/webhooks.html +4 -4
  71. package/dist/docs/hashmap.json +1 -1
  72. package/dist/docs/hillclimbing.html +4 -4
  73. package/dist/docs/index.html +6 -6
  74. package/dist/docs/quickstart.html +4 -4
  75. package/dist/docs/reference/agent-config.html +4 -4
  76. package/dist/docs/reference/channels.html +4 -4
  77. package/dist/docs/reference/cli.html +8 -7
  78. package/dist/docs/reference/connections.html +13 -8
  79. package/dist/docs/reference/hooks.html +4 -4
  80. package/dist/docs/reference/http-api.html +4 -4
  81. package/dist/docs/reference/instructions.html +4 -4
  82. package/dist/docs/reference/playground.html +4 -4
  83. package/dist/docs/reference/project-layout.html +4 -4
  84. package/dist/docs/reference/prompt.html +33 -0
  85. package/dist/docs/reference/schedules.html +4 -4
  86. package/dist/docs/reference/sessions.html +4 -4
  87. package/dist/docs/reference/skills.html +5 -5
  88. package/dist/docs/reference/subagents.html +4 -4
  89. package/dist/docs/reference/tools.html +20 -7
  90. package/dist/docs/scaffolding-agents.html +4 -4
  91. package/dist/docs/storage.html +4 -4
  92. package/dist/docs/troubleshooting.html +5 -5
  93. package/dist/evals.d.ts +39 -0
  94. package/dist/evals.d.ts.map +1 -1
  95. package/dist/index.d.ts +3 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -1
  98. package/dist/internal/cli-ax.d.ts.map +1 -1
  99. package/dist/internal/cli-ax.js +7 -4
  100. package/dist/internal/cli-mcp-oauth.d.ts +16 -0
  101. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
  102. package/dist/internal/cli-mcp-oauth.js +194 -0
  103. package/dist/internal/cli-mcp.d.ts +4 -1
  104. package/dist/internal/cli-mcp.d.ts.map +1 -1
  105. package/dist/internal/cli-mcp.js +19 -1
  106. package/dist/internal/corp-ca.d.ts +9 -0
  107. package/dist/internal/corp-ca.d.ts.map +1 -0
  108. package/dist/internal/corp-ca.js +44 -0
  109. package/dist/internal/cursor/account-mcp.d.ts +17 -4
  110. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  111. package/dist/internal/cursor/account-mcp.js +105 -17
  112. package/dist/internal/discovery.d.ts.map +1 -1
  113. package/dist/internal/discovery.js +64 -10
  114. package/dist/internal/distribution.d.ts.map +1 -1
  115. package/dist/internal/distribution.js +1 -0
  116. package/dist/internal/logs-client.js +1 -1
  117. package/dist/internal/mcp-host.d.ts.map +1 -1
  118. package/dist/internal/mcp-host.js +49 -26
  119. package/dist/internal/mcp-oauth.d.ts +88 -0
  120. package/dist/internal/mcp-oauth.d.ts.map +1 -0
  121. package/dist/internal/mcp-oauth.js +385 -0
  122. package/dist/internal/resolved-connections.d.ts +6 -0
  123. package/dist/internal/resolved-connections.d.ts.map +1 -1
  124. package/dist/internal/resolved-connections.js +10 -3
  125. package/dist/internal/sdk-runner.d.ts +9 -0
  126. package/dist/internal/sdk-runner.d.ts.map +1 -1
  127. package/dist/internal/sdk-runner.js +28 -2
  128. package/dist/internal/server.d.ts.map +1 -1
  129. package/dist/internal/server.js +24 -6
  130. package/dist/internal/session-engine.d.ts +30 -10
  131. package/dist/internal/session-engine.d.ts.map +1 -1
  132. package/dist/internal/session-engine.js +166 -74
  133. package/dist/internal/sessions-client.d.ts +16 -2
  134. package/dist/internal/sessions-client.d.ts.map +1 -1
  135. package/dist/internal/sessions-client.js +30 -13
  136. package/dist/internal/storage-coordinator.d.ts +11 -0
  137. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  138. package/dist/internal/storage-coordinator.js +30 -0
  139. package/dist/internal/stream-progress.d.ts.map +1 -1
  140. package/dist/internal/stream-progress.js +8 -4
  141. package/dist/internal/turn-governor.d.ts +54 -0
  142. package/dist/internal/turn-governor.d.ts.map +1 -0
  143. package/dist/internal/turn-governor.js +111 -0
  144. package/dist/playground/assets/{index-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
  145. package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
  146. package/dist/playground/index.html +2 -2
  147. package/dist/prompt.d.ts +39 -0
  148. package/dist/prompt.d.ts.map +1 -0
  149. package/dist/prompt.js +70 -0
  150. package/dist/storage-backends/cursor-hosted.d.ts +10 -0
  151. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  152. package/dist/storage-backends/cursor-hosted.js +14 -0
  153. package/dist/types.d.ts +93 -5
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +6 -0
  156. package/docs/README.md +4 -0
  157. package/docs/deployment.md +6 -1
  158. package/docs/guides/mcp-oauth.md +157 -0
  159. package/docs/reference/cli.md +30 -0
  160. package/docs/reference/connections.md +32 -2
  161. package/docs/reference/prompt.md +47 -0
  162. package/docs/reference/tools.md +21 -0
  163. package/docs/troubleshooting.md +11 -0
  164. package/package.json +9 -2
  165. package/skills/create-agent/SKILL.md +7 -3
  166. package/skills/framework-map/SKILL.md +1 -0
  167. package/skills/mcp-auth/SKILL.md +138 -0
  168. package/src/bin/agent-serve.ts +30 -3
  169. package/src/channels/github/github-channel.ts +2 -0
  170. package/src/channels/github/types.ts +4 -0
  171. package/src/channels/slack/cursor-account.ts +2 -0
  172. package/src/channels/slack/dispatch.ts +49 -0
  173. package/src/channels/slack/eval-directive.ts +348 -0
  174. package/src/channels/slack/index.ts +1 -0
  175. package/src/channels/slack/slack-channel.ts +3 -0
  176. package/src/channels/slack/types.ts +7 -0
  177. package/src/connections.ts +10 -5
  178. package/src/evals.ts +43 -0
  179. package/src/index.ts +3 -0
  180. package/src/internal/cli-ax.ts +10 -4
  181. package/src/internal/cli-mcp-oauth.ts +254 -0
  182. package/src/internal/cli-mcp.ts +25 -2
  183. package/src/internal/cursor/account-mcp.ts +153 -41
  184. package/src/internal/discovery.ts +70 -8
  185. package/src/internal/distribution.ts +1 -0
  186. package/src/internal/logs-client.ts +1 -1
  187. package/src/internal/mcp-host.ts +33 -9
  188. package/src/internal/mcp-oauth.ts +496 -0
  189. package/src/internal/resolved-connections.ts +20 -5
  190. package/src/internal/sdk-runner.ts +37 -1
  191. package/src/internal/server.ts +28 -6
  192. package/src/internal/session-engine.ts +192 -76
  193. package/src/internal/sessions-client.ts +51 -15
  194. package/src/internal/storage-coordinator.ts +36 -0
  195. package/src/internal/stream-progress.ts +8 -0
  196. package/src/internal/turn-governor.ts +145 -0
  197. package/src/prompt.ts +83 -0
  198. package/src/storage-backends/cursor-hosted.ts +21 -0
  199. package/src/types.ts +92 -6
  200. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +0 -1
@@ -45,6 +45,7 @@ import {
45
45
  type SessionEvent,
46
46
  type SessionEventPayload,
47
47
  type SessionInfo,
48
+ type SessionListSource,
48
49
  type SessionRecord,
49
50
  type SessionSummary,
50
51
  type SlackNudgeHostApi,
@@ -81,6 +82,7 @@ import type { AgentRunner } from "./sdk-runner.js";
81
82
  import { SessionStore } from "./session-store.js";
82
83
  import { StorageCoordinator } from "./storage-coordinator.js";
83
84
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
85
+ import { TurnGovernor } from "./turn-governor.js";
84
86
  import {
85
87
  buildAgentsMdContent,
86
88
  isNestedInGitRepo,
@@ -251,6 +253,8 @@ export class SessionEngine {
251
253
  private readonly turnAborts = new Map<string, AbortController>();
252
254
  /** Serializes turn admission per session (latest follow-up wins). */
253
255
  private readonly admissionChains = new Map<string, Promise<unknown>>();
256
+ /** FIFO turn slots under `concurrency.maxRunningTurns` (unlimited when unset). */
257
+ private readonly turnGovernor: TurnGovernor;
254
258
  /**
255
259
  * Per-session coalesce queue for `admission: "coalesce"` (Slack). Drained
256
260
  * into one follow-up turn when the active turn finishes without interrupt.
@@ -270,6 +274,7 @@ export class SessionEngine {
270
274
  private readonly approvals = new ApprovalGate();
271
275
  private readonly abCollector: ABCollector;
272
276
  private reminderApi: HostContext["reminders"];
277
+ private evalsApi: HostContext["evals"];
273
278
  private readonly kvApi: HostContext["kv"];
274
279
  /** Serve-time resolved connections (see {@link setResolvedConnections}). */
275
280
  private resolvedConnections: EngineResolvedConnection[] = [];
@@ -279,6 +284,7 @@ export class SessionEngine {
279
284
  this.stateRoot = options.stateRoot;
280
285
  this.runner = options.runner;
281
286
  this.logger = options.logger ?? (() => {});
287
+ this.turnGovernor = new TurnGovernor(options.project.agent.concurrency);
282
288
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
283
289
  this.nudgeStore = new SlackNudgeStore(
284
290
  join(options.stateRoot, "slack-nudges")
@@ -349,13 +355,14 @@ export class SessionEngine {
349
355
  }
350
356
  }
351
357
 
352
- /** Shared host services (MCP / GitHub / Slack / reminders / kv / Slack nudges). */
358
+ /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
353
359
  get host(): HostContext {
354
360
  return {
355
361
  mcp: this.mcpHost,
356
362
  github: this.platforms.github,
357
363
  slack: this.platforms.slack,
358
364
  reminders: this.reminderApi,
365
+ evals: this.evalsApi,
359
366
  kv: this.kvApi,
360
367
  slackNudges: this.slackNudgeHostApi(),
361
368
  };
@@ -383,6 +390,11 @@ export class SessionEngine {
383
390
  this.reminderApi = api;
384
391
  }
385
392
 
393
+ /** Attach the serve-host playground evals API (called from startServer). */
394
+ setEvals(api: HostContext["evals"]): void {
395
+ this.evalsApi = api;
396
+ }
397
+
386
398
  // ==========================================================================
387
399
  // Sending messages
388
400
  // ==========================================================================
@@ -778,11 +790,8 @@ export class SessionEngine {
778
790
  turnId,
779
791
  data: { text: message },
780
792
  });
781
- await this.appendEvent(sessionId, {
782
- type: "turn.started",
783
- turnId,
784
- data: {},
785
- });
793
+ // `turn.started` is appended by executeTurn once the turn holds a
794
+ // governor slot; saturated engines emit `turn.queued` first.
786
795
 
787
796
  const abort = new AbortController();
788
797
  this.turnAborts.set(sessionId, abort);
@@ -1000,65 +1009,91 @@ export class SessionEngine {
1000
1009
  };
1001
1010
  const mapper = new TurnEventMapper(emit);
1002
1011
 
1012
+ // Take a turn slot before any work happens. When every slot is busy the
1013
+ // turn waits FIFO — durable `turn.queued` marks the wait — and a
1014
+ // preempting follow-up (or interrupt) resolves the wait as cancelled
1015
+ // without the turn ever running.
1016
+ const permit = await this.turnGovernor.acquire({
1017
+ sessionId,
1018
+ signal,
1019
+ onQueued: (position) => {
1020
+ const maxRunningTurns = this.turnGovernor.maxRunningTurns ?? 0;
1021
+ emit({ type: "turn.queued", data: { position, maxRunningTurns } });
1022
+ this.logger(
1023
+ `[session] ${sessionId} ${turnId} queued at position ${position} (${maxRunningTurns} turn slot(s) busy)`
1024
+ );
1025
+ },
1026
+ });
1027
+
1003
1028
  let outcome: TurnOutcome;
1004
- try {
1005
- // Scaffold agent files into the session workspace even when the harness
1006
- // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1007
- // overlays are applied via agentForSession.
1008
- const agent = this.agentForSession(record);
1009
- await materializeWorkspace({
1010
- agent,
1011
- workspaceDir,
1012
- seed: isFirstTurn,
1013
- });
1014
- const pendingFiles = record.pendingWorkspaceFiles;
1015
- if (pendingFiles !== undefined && Object.keys(pendingFiles).length > 0) {
1016
- await writeWorkspaceFiles(harnessCwd, pendingFiles);
1017
- await this.updateRecord(sessionId, (current) => {
1018
- const { pendingWorkspaceFiles: _cleared, ...rest } = current;
1019
- return rest;
1029
+ if (permit === undefined) {
1030
+ // Preempted while waiting for a slot; settle as an interrupted turn.
1031
+ outcome = { status: "cancelled" };
1032
+ } else {
1033
+ emit({ type: "turn.started", data: {} });
1034
+ try {
1035
+ // Scaffold agent files into the session workspace even when the harness
1036
+ // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1037
+ // overlays are applied via agentForSession.
1038
+ const agent = this.agentForSession(record);
1039
+ await materializeWorkspace({
1040
+ agent,
1041
+ workspaceDir,
1042
+ seed: isFirstTurn,
1020
1043
  });
1044
+ const pendingFiles = record.pendingWorkspaceFiles;
1045
+ if (
1046
+ pendingFiles !== undefined &&
1047
+ Object.keys(pendingFiles).length > 0
1048
+ ) {
1049
+ await writeWorkspaceFiles(harnessCwd, pendingFiles);
1050
+ await this.updateRecord(sessionId, (current) => {
1051
+ const { pendingWorkspaceFiles: _cleared, ...rest } = current;
1052
+ return rest;
1053
+ });
1054
+ }
1055
+ // The harness only loads the scaffolded AGENTS.md when the session
1056
+ // workspace is its own project root. With a per-session cwd override
1057
+ // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1058
+ // inside a git repo re-roots harness rules at that repo — in both
1059
+ // cases deliver instructions inline on the first turn instead.
1060
+ const instructionsInvisibleToHarness =
1061
+ isFirstTurn &&
1062
+ (record.workspaceOverride !== undefined ||
1063
+ (await isNestedInGitRepo(harnessCwd)));
1064
+ const request = this.buildTurnRequest(
1065
+ record,
1066
+ agent,
1067
+ turnId,
1068
+ message,
1069
+ isFirstTurn,
1070
+ instructionsInvisibleToHarness,
1071
+ emit,
1072
+ async (update) => {
1073
+ mapper.handleUpdate(update);
1074
+ // Yield so the HTTP NDJSON writer can flush tool start/result
1075
+ // events while sibling parallel tools are still executing.
1076
+ if (
1077
+ update.type === "tool-call-started" ||
1078
+ update.type === "tool-call-completed" ||
1079
+ // Nested subagent progress (newer SDKs); string check keeps
1080
+ // typecheck green against older published `@cursor/sdk` pins.
1081
+ (update as { type: string }).type === "tool-call-delta"
1082
+ ) {
1083
+ await yieldEventLoop();
1084
+ }
1085
+ },
1086
+ signal
1087
+ );
1088
+ outcome = await this.runner.runTurn(request);
1089
+ } catch (error) {
1090
+ this.approvals.cancelSession(sessionId, "deny");
1091
+ outcome = { status: "error", errorMessage: describeError(error) };
1092
+ } finally {
1093
+ permit.release();
1094
+ this.approvals.cancelSession(sessionId, "deny");
1095
+ await this.clearPendingApprovals(sessionId);
1021
1096
  }
1022
- // The harness only loads the scaffolded AGENTS.md when the session
1023
- // workspace is its own project root. With a per-session cwd override
1024
- // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1025
- // inside a git repo re-roots harness rules at that repo — in both
1026
- // cases deliver instructions inline on the first turn instead.
1027
- const instructionsInvisibleToHarness =
1028
- isFirstTurn &&
1029
- (record.workspaceOverride !== undefined ||
1030
- (await isNestedInGitRepo(harnessCwd)));
1031
- const request = this.buildTurnRequest(
1032
- record,
1033
- agent,
1034
- turnId,
1035
- message,
1036
- isFirstTurn,
1037
- instructionsInvisibleToHarness,
1038
- emit,
1039
- async (update) => {
1040
- mapper.handleUpdate(update);
1041
- // Yield so the HTTP NDJSON writer can flush tool start/result
1042
- // events while sibling parallel tools are still executing.
1043
- if (
1044
- update.type === "tool-call-started" ||
1045
- update.type === "tool-call-completed" ||
1046
- // Nested subagent progress (newer SDKs); string check keeps
1047
- // typecheck green against older published `@cursor/sdk` pins.
1048
- (update as { type: string }).type === "tool-call-delta"
1049
- ) {
1050
- await yieldEventLoop();
1051
- }
1052
- },
1053
- signal
1054
- );
1055
- outcome = await this.runner.runTurn(request);
1056
- } catch (error) {
1057
- this.approvals.cancelSession(sessionId, "deny");
1058
- outcome = { status: "error", errorMessage: describeError(error) };
1059
- } finally {
1060
- this.approvals.cancelSession(sessionId, "deny");
1061
- await this.clearPendingApprovals(sessionId);
1062
1097
  }
1063
1098
 
1064
1099
  mapper.finish();
@@ -1223,38 +1258,42 @@ export class SessionEngine {
1223
1258
  };
1224
1259
  }
1225
1260
 
1226
- /**
1227
- * MCP servers for a turn: authored concrete connections plus resolved
1228
- * serve-time resolved connections (peers, Cursor account bridge).
1229
- * Resolved connections pick their URL by runtime — loopback for local
1230
- * turns, `cloudUrl` for cloud turns — and are omitted from cloud turns
1231
- * when no cloud URL exists unless a `servers` filter requires the bridge
1232
- * (see {@link assertCloudCanHonorAccountServersFilters}).
1233
- */
1261
+ /** Authored + resolved MCP servers for a turn; skips `hostOnly`. */
1234
1262
  private buildMcpServers(
1235
1263
  runtime: "local" | "cloud"
1236
1264
  ): Record<string, ConnectionTransport> {
1265
+ const hostOnlyNames = new Set(
1266
+ this.project.agent.connections
1267
+ .filter((connection) => connection.hostOnly === true)
1268
+ .map((connection) => connection.name)
1269
+ );
1237
1270
  assertCloudCanHonorAccountServersFilters({
1238
1271
  runtime,
1239
- resolvedConnections: this.resolvedConnections,
1272
+ resolvedConnections: this.resolvedConnections.filter(
1273
+ (resolved) => !hostOnlyNames.has(resolved.name)
1274
+ ),
1240
1275
  });
1241
1276
  const servers: Record<string, ConnectionTransport> = {};
1242
1277
  for (const connection of this.project.agent.connections) {
1278
+ if (hostOnlyNames.has(connection.name)) {
1279
+ continue;
1280
+ }
1243
1281
  if (isSymbolicConnectionTransport(connection.transport)) {
1244
1282
  continue;
1245
1283
  }
1246
1284
  servers[connection.name] = connection.transport;
1247
1285
  }
1248
1286
  for (const resolved of this.resolvedConnections) {
1287
+ if (hostOnlyNames.has(resolved.name)) {
1288
+ continue;
1289
+ }
1249
1290
  const url = runtime === "cloud" ? resolved.cloudUrl : resolved.localUrl;
1250
1291
  if (url === undefined) {
1251
1292
  continue;
1252
1293
  }
1253
1294
  servers[resolved.name] = {
1254
1295
  url,
1255
- ...(resolved.headers === undefined
1256
- ? {}
1257
- : { headers: resolved.headers }),
1296
+ headers: resolved.headers,
1258
1297
  };
1259
1298
  }
1260
1299
  return servers;
@@ -1827,13 +1866,88 @@ export class SessionEngine {
1827
1866
  * observability surface across every principal (channel-driven runs,
1828
1867
  * schedule/task sessions, other callers). The route auth chain still
1829
1868
  * gates who may call this at all.
1869
+ *
1870
+ * When the project uses `cursorHostedStorage`, prefers durable Bugbot rows
1871
+ * (merged with any newer local records not flushed yet). Falls back to the
1872
+ * local `--state-root` store when the sink is unavailable.
1830
1873
  */
1831
1874
  async listSessions(
1832
1875
  caller: AuthContext | null,
1833
1876
  options?: { includeAll?: boolean }
1834
1877
  ): Promise<SessionSummary[]> {
1878
+ return (await this.listSessionsWithSource(caller, options)).sessions;
1879
+ }
1880
+
1881
+ /**
1882
+ * Same as {@link listSessions}, plus which store answered
1883
+ * (`cursor-hosted` = durable DB via the control-plane proxy; `local` =
1884
+ * `--state-root`).
1885
+ */
1886
+ async listSessionsWithSource(
1887
+ caller: AuthContext | null,
1888
+ options?: { includeAll?: boolean }
1889
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
1835
1890
  const includeAll = options?.includeAll ?? false;
1891
+ if (this.storage?.isCursorHosted === true) {
1892
+ try {
1893
+ const records = await this.mergeStoredAndLocalSessions();
1894
+ return {
1895
+ sessions: this.summarizeSessionRecords(records, caller, includeAll),
1896
+ source: "cursor-hosted",
1897
+ };
1898
+ } catch (error) {
1899
+ this.logger(
1900
+ `[agentkit] cursor-hosted session list failed; falling back to local: ${describeError(error)}`
1901
+ );
1902
+ }
1903
+ }
1836
1904
  const records = await this.sessions.list();
1905
+ // Local store is createdAt-asc; reverse so newest created leads.
1906
+ return {
1907
+ sessions: this.summarizeSessionRecords(
1908
+ [...records].reverse(),
1909
+ caller,
1910
+ includeAll
1911
+ ),
1912
+ source: "local",
1913
+ };
1914
+ }
1915
+
1916
+ /**
1917
+ * Durable sink rows plus local records (local wins on the same id when
1918
+ * newer — covers debounce windows that have not flushed yet). Newest
1919
+ * `updatedAt` first.
1920
+ */
1921
+ private async mergeStoredAndLocalSessions(): Promise<SessionRecord[]> {
1922
+ const storage = this.storage;
1923
+ if (storage === undefined) {
1924
+ throw new Error("storage is not configured");
1925
+ }
1926
+ const stored = await storage.listSessionsFromSink();
1927
+ const local = await this.sessions.list();
1928
+ const byId = new Map<string, SessionRecord>();
1929
+ for (const record of stored) {
1930
+ byId.set(record.sessionId, record);
1931
+ }
1932
+ for (const record of local) {
1933
+ const existing = byId.get(record.sessionId);
1934
+ if (
1935
+ existing === undefined ||
1936
+ record.updatedAt.localeCompare(existing.updatedAt) >= 0
1937
+ ) {
1938
+ byId.set(record.sessionId, record);
1939
+ }
1940
+ }
1941
+ return [...byId.values()].sort((a, b) =>
1942
+ b.updatedAt.localeCompare(a.updatedAt)
1943
+ );
1944
+ }
1945
+
1946
+ private summarizeSessionRecords(
1947
+ records: readonly SessionRecord[],
1948
+ caller: AuthContext | null,
1949
+ includeAll: boolean
1950
+ ): SessionSummary[] {
1837
1951
  return records
1838
1952
  .filter((record) => includeAll || samePrincipal(record.auth, caller))
1839
1953
  .map((record) => {
@@ -1847,6 +1961,9 @@ export class SessionEngine {
1847
1961
  running:
1848
1962
  this.activeTurns.has(record.sessionId) ||
1849
1963
  this.pendingTurns.has(record.sessionId),
1964
+ queued: this.turnGovernor.limited
1965
+ ? this.turnGovernor.isQueued(record.sessionId)
1966
+ : undefined,
1850
1967
  createdAt: record.createdAt,
1851
1968
  updatedAt: record.updatedAt,
1852
1969
  continuationToken:
@@ -1858,8 +1975,7 @@ export class SessionEngine {
1858
1975
  ),
1859
1976
  ...(externalUrl === undefined ? {} : { externalUrl }),
1860
1977
  };
1861
- })
1862
- .reverse();
1978
+ });
1863
1979
  }
1864
1980
 
1865
1981
  /**
@@ -4,15 +4,33 @@
4
4
  * Customer-facing errors never include hosting URLs or infra jargon.
5
5
  */
6
6
 
7
- import type { SessionEvent, SessionSummary } from "../types.js";
7
+ import type {
8
+ SessionEvent,
9
+ SessionListSource,
10
+ SessionSummary,
11
+ } from "../types.js";
8
12
  import { sanitizeCustomerError } from "./logs-client.js";
9
13
  import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
10
14
 
15
+ export interface ListedSessions {
16
+ sessions: SessionSummary[];
17
+ /** Present when the server reports which store answered. */
18
+ source?: SessionListSource;
19
+ }
20
+
11
21
  export async function listSessions(args: {
12
22
  baseUrl: string;
13
23
  auth?: RequestAuth;
14
24
  fetchImpl?: typeof fetch;
15
25
  }): Promise<SessionSummary[]> {
26
+ return (await listSessionsWithSource(args)).sessions;
27
+ }
28
+
29
+ export async function listSessionsWithSource(args: {
30
+ baseUrl: string;
31
+ auth?: RequestAuth;
32
+ fetchImpl?: typeof fetch;
33
+ }): Promise<ListedSessions> {
16
34
  const baseUrl = args.baseUrl.replace(/\/$/, "");
17
35
  const fetchImpl = args.fetchImpl ?? fetch;
18
36
  let resp: Response;
@@ -30,18 +48,21 @@ export async function listSessions(args: {
30
48
  if (!resp.ok) {
31
49
  throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
32
50
  }
33
- const body = (await resp.json()) as { sessions?: unknown };
34
- if (!Array.isArray(body.sessions)) {
35
- return [];
36
- }
37
- const out: SessionSummary[] = [];
38
- for (const row of body.sessions) {
39
- const parsed = parseSessionSummary(row);
40
- if (parsed !== undefined) {
41
- out.push(parsed);
51
+ const body = (await resp.json()) as { sessions?: unknown; source?: unknown };
52
+ const sessions: SessionSummary[] = [];
53
+ if (Array.isArray(body.sessions)) {
54
+ for (const row of body.sessions) {
55
+ const parsed = parseSessionSummary(row);
56
+ if (parsed !== undefined) {
57
+ sessions.push(parsed);
58
+ }
42
59
  }
43
60
  }
44
- return out;
61
+ const source =
62
+ body.source === "cursor-hosted" || body.source === "local"
63
+ ? body.source
64
+ : undefined;
65
+ return source === undefined ? { sessions } : { sessions, source };
45
66
  }
46
67
 
47
68
  export async function fetchSessionEvents(args: {
@@ -144,6 +165,7 @@ function parseSessionSummary(value: unknown): SessionSummary | undefined {
144
165
  title: typeof row.title === "string" ? row.title : undefined,
145
166
  turnCount: typeof row.turnCount === "number" ? row.turnCount : 0,
146
167
  running: row.running === true,
168
+ queued: typeof row.queued === "boolean" ? row.queued : undefined,
147
169
  createdAt: typeof row.createdAt === "string" ? row.createdAt : "",
148
170
  updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
149
171
  continuationToken,
@@ -186,10 +208,24 @@ export async function resolvePreviousSessionId(args: {
186
208
  return pickPreviousSession(sessions)?.sessionId;
187
209
  }
188
210
 
211
+ /** Human label for {@link SessionListSource} in CLI output. */
212
+ export function formatSessionListSource(source: SessionListSource): string {
213
+ return source === "cursor-hosted"
214
+ ? "Listing from cursor-hosted storage (durable DB)."
215
+ : "Listing from local session store.";
216
+ }
217
+
189
218
  /** Format a sessions table (id, channel, mode, turns, running, updated). */
190
- export function formatSessionsTable(sessions: SessionSummary[]): string {
219
+ export function formatSessionsTable(
220
+ sessions: SessionSummary[],
221
+ options?: { source?: SessionListSource }
222
+ ): string {
223
+ const sourceLine =
224
+ options?.source === undefined
225
+ ? ""
226
+ : `${formatSessionListSource(options.source)}\n`;
191
227
  if (sessions.length === 0) {
192
- return "No sessions.\n";
228
+ return `${sourceLine}No sessions.\n`;
193
229
  }
194
230
  const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
195
231
  const rows = sessions.map((s) => [
@@ -197,10 +233,10 @@ export function formatSessionsTable(sessions: SessionSummary[]): string {
197
233
  s.channelId,
198
234
  s.mode,
199
235
  String(s.turnCount),
200
- s.running ? "yes" : "no",
236
+ s.queued === true ? "queued" : s.running ? "yes" : "no",
201
237
  s.updatedAt,
202
238
  ]);
203
- return `${renderTable(header, rows)}\n`;
239
+ return `${sourceLine}${renderTable(header, rows)}\n`;
204
240
  }
205
241
 
206
242
  function renderTable(header: string[], rows: string[][]): string {
@@ -24,6 +24,7 @@ import {
24
24
  type StorageDefinition,
25
25
  storageKeys,
26
26
  } from "../storage.js";
27
+ import { isCursorHostedStorage } from "../storage-backends/cursor-hosted.js";
27
28
  import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
28
29
  import type { ABSnapshot } from "./ab-snapshot.js";
29
30
  import { describeError } from "./describe-error.js";
@@ -261,6 +262,14 @@ export class StorageCoordinator {
261
262
  return this.definition.list !== undefined;
262
263
  }
263
264
 
265
+ /**
266
+ * True when the authored sink is `cursorHostedStorage` (Bugbot via the
267
+ * control-plane proxy). `GET /v1/sessions` prefers this durable list.
268
+ */
269
+ get isCursorHosted(): boolean {
270
+ return isCursorHostedStorage(this.definition);
271
+ }
272
+
264
273
  /**
265
274
  * Saved session records for this agent (startup restore). Errors are
266
275
  * logged and read as "nothing saved" — a broken store must never block
@@ -275,6 +284,33 @@ export class StorageCoordinator {
275
284
  .filter((record) => record != null);
276
285
  }
277
286
 
287
+ /**
288
+ * Session records from the sink for the sessions API. Unlike
289
+ * {@link listSessions}, missing `list` or a thrown sink error **propagates**
290
+ * so the caller can fall back to the local session store.
291
+ */
292
+ async listSessionsFromSink(): Promise<SessionRecord[]> {
293
+ const list = this.definition.list;
294
+ if (list === undefined) {
295
+ throw new Error("storage sink has no list");
296
+ }
297
+ const entries = await list(
298
+ storageKeys.sessionPrefix(this.agentName),
299
+ this.context("restore")
300
+ );
301
+ if (!Array.isArray(entries)) {
302
+ throw new Error("storage list returned a non-array");
303
+ }
304
+ return entries
305
+ .map((entry) => entry.value as unknown as SessionRecord)
306
+ .filter(
307
+ (record) =>
308
+ record != null &&
309
+ typeof record.sessionId === "string" &&
310
+ record.sessionId !== ""
311
+ );
312
+ }
313
+
278
314
  /**
279
315
  * Saved event stream for one session: chunk values concatenated in key
280
316
  * order (chunks are keyed by their first event's index, zero-padded, so
@@ -50,6 +50,14 @@ export function createStreamProgress(options?: {
50
50
  }
51
51
  break;
52
52
  }
53
+ case "turn.queued": {
54
+ write(
55
+ c.dim(
56
+ ` turn ${event.turnId ?? "?"} queued (position ${event.data.position}, max ${event.data.maxRunningTurns} running)\n`
57
+ )
58
+ );
59
+ break;
60
+ }
53
61
  case "turn.started": {
54
62
  turnClock = Date.now();
55
63
  if (event.turnId !== undefined) {