@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.
- package/AGENTS.md +1 -0
- package/dist/bin/agent-serve.js +26 -3
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +2 -1
- package/dist/channels/github/types.d.ts +4 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +1 -1
- package/dist/channels/slack/dispatch.d.ts +13 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +36 -11
- package/dist/channels/slack/eval-directive.d.ts +63 -0
- package/dist/channels/slack/eval-directive.d.ts.map +1 -0
- package/dist/channels/slack/eval-directive.js +283 -0
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -2
- package/dist/channels/slack/types.d.ts +7 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +10 -5
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +10 -5
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.Ciosf24M.js → app.BcXUGca7.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.BnvulOX2.js} +2 -2
- package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
- package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
- package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
- package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
- package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
- package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.B74NrVzb.js} +5 -4
- package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.CsSr3rDz.js} +9 -4
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
- package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
- package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
- package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
- package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
- package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
- package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +4 -4
- package/dist/docs/example-agents/benny.html +4 -4
- package/dist/docs/example-agents/bugbot.html +4 -4
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +4 -4
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +4 -4
- package/dist/docs/example-agents/security-reviewer.html +4 -4
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +53 -0
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +8 -7
- package/dist/docs/reference/connections.html +13 -8
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +33 -0
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +5 -5
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +20 -7
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/evals.d.ts +39 -0
- package/dist/evals.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +7 -4
- package/dist/internal/cli-mcp-oauth.d.ts +16 -0
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
- package/dist/internal/cli-mcp-oauth.js +194 -0
- package/dist/internal/cli-mcp.d.ts +4 -1
- package/dist/internal/cli-mcp.d.ts.map +1 -1
- package/dist/internal/cli-mcp.js +19 -1
- package/dist/internal/corp-ca.d.ts +9 -0
- package/dist/internal/corp-ca.d.ts.map +1 -0
- package/dist/internal/corp-ca.js +44 -0
- package/dist/internal/cursor/account-mcp.d.ts +17 -4
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +105 -17
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +64 -10
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/logs-client.js +1 -1
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +49 -26
- package/dist/internal/mcp-oauth.d.ts +88 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -0
- package/dist/internal/mcp-oauth.js +385 -0
- package/dist/internal/resolved-connections.d.ts +6 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +10 -3
- package/dist/internal/sdk-runner.d.ts +9 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +28 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +24 -6
- package/dist/internal/session-engine.d.ts +30 -10
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +166 -74
- package/dist/internal/sessions-client.d.ts +16 -2
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +30 -13
- package/dist/internal/storage-coordinator.d.ts +11 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +30 -0
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +8 -4
- package/dist/internal/turn-governor.d.ts +54 -0
- package/dist/internal/turn-governor.d.ts.map +1 -0
- package/dist/internal/turn-governor.js +111 -0
- package/dist/playground/assets/{index-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
- package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
- package/dist/playground/index.html +2 -2
- package/dist/prompt.d.ts +39 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +70 -0
- package/dist/storage-backends/cursor-hosted.d.ts +10 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +14 -0
- package/dist/types.d.ts +93 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/docs/README.md +4 -0
- package/docs/deployment.md +6 -1
- package/docs/guides/mcp-oauth.md +157 -0
- package/docs/reference/cli.md +30 -0
- package/docs/reference/connections.md +32 -2
- package/docs/reference/prompt.md +47 -0
- package/docs/reference/tools.md +21 -0
- package/docs/troubleshooting.md +11 -0
- package/package.json +9 -2
- package/skills/create-agent/SKILL.md +7 -3
- package/skills/framework-map/SKILL.md +1 -0
- package/skills/mcp-auth/SKILL.md +138 -0
- package/src/bin/agent-serve.ts +30 -3
- package/src/channels/github/github-channel.ts +2 -0
- package/src/channels/github/types.ts +4 -0
- package/src/channels/slack/cursor-account.ts +2 -0
- package/src/channels/slack/dispatch.ts +49 -0
- package/src/channels/slack/eval-directive.ts +348 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/types.ts +7 -0
- package/src/connections.ts +10 -5
- package/src/evals.ts +43 -0
- package/src/index.ts +3 -0
- package/src/internal/cli-ax.ts +10 -4
- package/src/internal/cli-mcp-oauth.ts +254 -0
- package/src/internal/cli-mcp.ts +25 -2
- package/src/internal/cursor/account-mcp.ts +153 -41
- package/src/internal/discovery.ts +70 -8
- package/src/internal/distribution.ts +1 -0
- package/src/internal/logs-client.ts +1 -1
- package/src/internal/mcp-host.ts +33 -9
- package/src/internal/mcp-oauth.ts +496 -0
- package/src/internal/resolved-connections.ts +20 -5
- package/src/internal/sdk-runner.ts +37 -1
- package/src/internal/server.ts +28 -6
- package/src/internal/session-engine.ts +192 -76
- package/src/internal/sessions-client.ts +51 -15
- package/src/internal/storage-coordinator.ts +36 -0
- package/src/internal/stream-progress.ts +8 -0
- package/src/internal/turn-governor.ts +145 -0
- package/src/prompt.ts +83 -0
- package/src/storage-backends/cursor-hosted.ts +21 -0
- package/src/types.ts +92 -6
- 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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
1005
|
-
//
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
agent
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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) {
|