@cursor/july 0.1.62 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +8 -0
- package/dist/channels/slack/channel-watch.d.ts +2 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +7 -3
- package/dist/channels/slack/dispatch.d.ts +9 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +41 -21
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +1 -0
- package/dist/channels/slack/nudge-store.d.ts +16 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -1
- package/dist/channels/slack/nudge-store.js +53 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -1
- package/dist/channels/slack/types.d.ts +22 -5
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.B-biogkN.js → app.C5XUTCw9.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DvUR3lv9.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.CGDTAYHY.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.lPixh250.js} +2 -2
- package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Bwswbm6H.js} +13 -3
- package/dist/docs/assets/{reference_connections.md.CSQiNzre.lean.js → reference_connections.md.Bwswbm6H.lean.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.Wmvb7Lob.js} +2 -2
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.Wmvb7Lob.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +15 -5
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +5 -5
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/files-backends/agent-store-presigned-url.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/advertise-tools.d.ts +74 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -0
- package/dist/internal/advertise-tools.js +152 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +11 -3
- package/dist/internal/cli-manifest.d.ts +19 -0
- package/dist/internal/cli-manifest.d.ts.map +1 -0
- package/dist/internal/cli-manifest.js +49 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +9 -1
- package/dist/internal/cursor-agent-template.d.ts +9 -0
- package/dist/internal/cursor-agent-template.d.ts.map +1 -0
- package/dist/internal/cursor-agent-template.js +37 -0
- package/dist/internal/deploy-client.d.ts +8 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +21 -3
- package/dist/internal/deploy-manifest.d.ts +85 -0
- package/dist/internal/deploy-manifest.d.ts.map +1 -0
- package/dist/internal/deploy-manifest.js +106 -0
- package/dist/internal/deployment-scm-event-stream.d.ts +113 -0
- package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
- package/dist/internal/deployment-scm-event-stream.js +460 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +69 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +5 -17
- package/dist/internal/mcp-host.d.ts +18 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +41 -5
- package/dist/internal/sdk-runner.d.ts +1 -1
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +36 -13
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +18 -0
- package/dist/internal/session-engine.d.ts +66 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +430 -110
- package/dist/internal/shallow-clone.d.ts +115 -0
- package/dist/internal/shallow-clone.d.ts.map +1 -0
- package/dist/internal/shallow-clone.js +292 -0
- package/dist/internal/slack-provision-client.d.ts +2 -0
- package/dist/internal/slack-provision-client.d.ts.map +1 -1
- package/dist/internal/slack-provision-client.js +1 -0
- package/dist/internal/turn-lease-store.d.ts +44 -0
- package/dist/internal/turn-lease-store.d.ts.map +1 -0
- package/dist/internal/turn-lease-store.js +139 -0
- package/dist/internal/workspace.d.ts +12 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +28 -0
- package/dist/playground/assets/index-CoL7xCJl.css +1 -0
- package/dist/playground/assets/index-yxs1heea.js +85 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-protocol.d.ts +122 -0
- package/dist/storage-protocol.d.ts.map +1 -0
- package/dist/storage-protocol.js +173 -0
- package/dist/storage.d.ts +2 -72
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -82
- package/dist/types.d.ts +113 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/reference/connections.md +80 -0
- package/docs/reference/tools.md +14 -0
- package/package.json +4 -2
- package/src/bin/agent-serve.ts +9 -0
- package/src/channels/slack/channel-watch.ts +6 -3
- package/src/channels/slack/dispatch.ts +40 -3
- package/src/channels/slack/manifest.ts +1 -0
- package/src/channels/slack/nudge-store.ts +56 -0
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/types.ts +22 -5
- package/src/index.ts +5 -0
- package/src/internal/advertise-tools.ts +229 -0
- package/src/internal/cli-deploy.ts +22 -1
- package/src/internal/cli-manifest.ts +63 -0
- package/src/internal/cli-slack.ts +10 -1
- package/src/internal/cursor-agent-template.ts +46 -0
- package/src/internal/deploy-client.ts +35 -6
- package/src/internal/deploy-manifest.ts +167 -0
- package/src/internal/deployment-scm-event-stream.ts +577 -0
- package/src/internal/discovery.ts +85 -0
- package/src/internal/init-project.ts +11 -20
- package/src/internal/mcp-host.ts +66 -9
- package/src/internal/sdk-runner.ts +50 -11
- package/src/internal/server.ts +19 -0
- package/src/internal/session-engine.ts +385 -19
- package/src/internal/shallow-clone.ts +407 -0
- package/src/internal/slack-provision-client.ts +3 -0
- package/src/internal/turn-lease-store.ts +147 -0
- package/src/internal/workspace.ts +27 -0
- package/src/storage-protocol.ts +247 -0
- package/src/storage.ts +16 -110
- package/src/types.ts +117 -3
- package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
- package/dist/playground/assets/index-B4rcOgeP.css +0 -1
- package/dist/playground/assets/index-DXcY42Jv.js +0 -85
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
resolveABMaxPlaygroundSessions,
|
|
18
18
|
resolveABSplit,
|
|
19
19
|
} from "../ab.js";
|
|
20
|
+
import { SLACK_CHANNEL_ID } from "../channels/slack/constants.js";
|
|
21
|
+
import { materializeSlackNudgeCarrier } from "../channels/slack/nudge-carrier.js";
|
|
20
22
|
import {
|
|
21
23
|
SlackAskDedupeStore,
|
|
22
24
|
SlackNudgeStore,
|
|
@@ -73,6 +75,7 @@ import {
|
|
|
73
75
|
import { ABCollector } from "./ab-collector.js";
|
|
74
76
|
import { foldABStream } from "./ab-fold.js";
|
|
75
77
|
import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
78
|
+
import { resolveAdvertisedTools } from "./advertise-tools.js";
|
|
76
79
|
import { ApprovalGate } from "./approval-gate.js";
|
|
77
80
|
import { ArtifactsStore } from "./artifacts-store.js";
|
|
78
81
|
import { SessionAuthError, samePrincipal } from "./auth.js";
|
|
@@ -132,9 +135,11 @@ import {
|
|
|
132
135
|
roundCostUsd,
|
|
133
136
|
} from "./turn-cost.js";
|
|
134
137
|
import { TurnGovernor } from "./turn-governor.js";
|
|
138
|
+
import { TurnLeaseStore } from "./turn-lease-store.js";
|
|
135
139
|
import {
|
|
136
140
|
buildAgentsMdContent,
|
|
137
141
|
buildCloudMemorySection,
|
|
142
|
+
ensureOwnGitRoot,
|
|
138
143
|
isNestedInGitRepo,
|
|
139
144
|
materializeWorkspace,
|
|
140
145
|
SERVER_TOOLS_MCP_SERVER_NAME,
|
|
@@ -318,6 +323,8 @@ export class SessionEngine {
|
|
|
318
323
|
readonly nudgeStore: SlackNudgeStore;
|
|
319
324
|
/** Durable `channelId:ts` ask claims under `<stateRoot>/slack-asks`. */
|
|
320
325
|
readonly askDedupe: SlackAskDedupeStore;
|
|
326
|
+
/** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
|
|
327
|
+
readonly turnLeases: TurnLeaseStore;
|
|
321
328
|
/**
|
|
322
329
|
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
323
330
|
* authored. All durable state changes funnel through it (session
|
|
@@ -342,6 +349,12 @@ export class SessionEngine {
|
|
|
342
349
|
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
343
350
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
344
351
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
352
|
+
/**
|
|
353
|
+
* Delivered-nudge count per session at the last tool boundary. The carrier
|
|
354
|
+
* drains out-of-process, so a growing count is how the engine learns a
|
|
355
|
+
* follow-up reached the agent.
|
|
356
|
+
*/
|
|
357
|
+
private readonly deliveredNudges = new Map<string, number>();
|
|
345
358
|
/** Aborts the in-flight turn per session so a follow-up can preempt it. */
|
|
346
359
|
private readonly turnAborts = new Map<string, AbortController>();
|
|
347
360
|
/** Serializes turn admission per session (latest follow-up wins). */
|
|
@@ -415,6 +428,9 @@ export class SessionEngine {
|
|
|
415
428
|
this.askDedupe = new SlackAskDedupeStore(
|
|
416
429
|
join(options.stateRoot, "slack-asks")
|
|
417
430
|
);
|
|
431
|
+
this.turnLeases = new TurnLeaseStore(
|
|
432
|
+
join(options.stateRoot, "turn-leases")
|
|
433
|
+
);
|
|
418
434
|
this.logs = new EventLogStore(
|
|
419
435
|
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
420
436
|
(sessionId, error) =>
|
|
@@ -731,6 +747,110 @@ export class SessionEngine {
|
|
|
731
747
|
return record;
|
|
732
748
|
}
|
|
733
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Install the Slack nudge carrier into a local session's harness cwd, so a
|
|
752
|
+
* follow-up that arrives mid-turn reaches the running agent at its next tool
|
|
753
|
+
* boundary instead of waiting for the boundary turn.
|
|
754
|
+
*
|
|
755
|
+
* Slack-only and local-only: the carrier is a Cursor hook in the workspace,
|
|
756
|
+
* and a cloud VM has neither the workspace nor the nudge store.
|
|
757
|
+
*
|
|
758
|
+
* The harness only reads these hooks when the workspace is its own git root;
|
|
759
|
+
* nested in an outer repo it reads that repo's instead and the carrier never
|
|
760
|
+
* runs, leaving follow-ups to drain at the turn boundary as before.
|
|
761
|
+
*/
|
|
762
|
+
private async materializeNudgeCarrier(
|
|
763
|
+
record: SessionRecord,
|
|
764
|
+
harnessCwd: string
|
|
765
|
+
): Promise<void> {
|
|
766
|
+
if (
|
|
767
|
+
record.channelId !== SLACK_CHANNEL_ID ||
|
|
768
|
+
record.continuationKey === null ||
|
|
769
|
+
record.continuationKey === "" ||
|
|
770
|
+
// Borrowed: a PR worktree, or a checkout shared by every session. Hooks
|
|
771
|
+
// written there dirty a tree this session did not create, and a shared
|
|
772
|
+
// cwd holds one continuation pointer for all of them — the carrier would
|
|
773
|
+
// inject one thread's follow-ups into another thread's turn. Nothing is
|
|
774
|
+
// lost: a borrowed checkout is its own git root, so the harness reads its
|
|
775
|
+
// hooks rather than these, and follow-ups drain at the turn boundary.
|
|
776
|
+
this.hasBorrowedWorkspace(record)
|
|
777
|
+
) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
await materializeSlackNudgeCarrier({
|
|
782
|
+
harnessCwd,
|
|
783
|
+
nudgeRootDir: join(this.stateRoot, "slack-nudges"),
|
|
784
|
+
continuationKey: record.continuationKey,
|
|
785
|
+
});
|
|
786
|
+
} catch (error) {
|
|
787
|
+
// Best-effort: without the carrier, follow-ups still drain at the turn
|
|
788
|
+
// boundary — one turn later, never lost.
|
|
789
|
+
this.logger(
|
|
790
|
+
`[session] slack nudge carrier not installed: ${describeError(error)}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Report a mid-turn follow-up reaching the agent, as `nudge.delivered`.
|
|
797
|
+
*
|
|
798
|
+
* The workspace carrier drains `pending/` into `delivered/` out of process,
|
|
799
|
+
* so the engine watches the delivered count grow rather than the turn's own
|
|
800
|
+
* events. Counting rather than edge-detecting on "still queued" matters:
|
|
801
|
+
* the carrier runs at the same tool boundary this check does, so a nudge
|
|
802
|
+
* queued and drained between two readings is never seen as pending.
|
|
803
|
+
*
|
|
804
|
+
* Channels need this to move a live progress message below the follow-up —
|
|
805
|
+
* Slack cannot reorder messages.
|
|
806
|
+
*/
|
|
807
|
+
private async emitDeliveredNudges(
|
|
808
|
+
record: SessionRecord,
|
|
809
|
+
emit: (event: SessionEventPayload) => void
|
|
810
|
+
): Promise<void> {
|
|
811
|
+
const delivered = await this.readDeliveredNudges(record);
|
|
812
|
+
if (delivered === undefined) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const previous = this.deliveredNudges.get(record.sessionId);
|
|
816
|
+
this.deliveredNudges.set(record.sessionId, delivered);
|
|
817
|
+
if (previous !== undefined && delivered > previous) {
|
|
818
|
+
emit({ type: "nudge.delivered", data: {} });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Record the thread's delivered count before the turn runs. Earlier turns
|
|
824
|
+
* leave their own records behind, so only growth from here is this turn's.
|
|
825
|
+
*/
|
|
826
|
+
private async baselineDeliveredNudges(record: SessionRecord): Promise<void> {
|
|
827
|
+
const delivered = await this.readDeliveredNudges(record);
|
|
828
|
+
if (delivered !== undefined) {
|
|
829
|
+
this.deliveredNudges.set(record.sessionId, delivered);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private async readDeliveredNudges(
|
|
834
|
+
record: SessionRecord
|
|
835
|
+
): Promise<number | undefined> {
|
|
836
|
+
const continuationKey = record.continuationKey;
|
|
837
|
+
// Same gate as the carrier: only Slack queues follow-ups mid-turn, and
|
|
838
|
+
// this runs on every tool boundary of every turn.
|
|
839
|
+
if (
|
|
840
|
+
record.channelId !== SLACK_CHANNEL_ID ||
|
|
841
|
+
continuationKey === null ||
|
|
842
|
+
continuationKey === ""
|
|
843
|
+
) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
return await this.nudgeStore.deliveredCount(continuationKey);
|
|
848
|
+
} catch {
|
|
849
|
+
// A store read must never break the turn it is observing.
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
734
854
|
private slackNudgeHostApi(): SlackNudgeHostApi {
|
|
735
855
|
return {
|
|
736
856
|
append: (input) => this.nudgeStore.append(input),
|
|
@@ -1126,6 +1246,67 @@ export class SessionEngine {
|
|
|
1126
1246
|
this.coalesceQueues.delete(sessionId);
|
|
1127
1247
|
}
|
|
1128
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* Drop queued follow-ups the nudge carrier already carried into the turn
|
|
1251
|
+
* that just finished — the contract {@link SendMessageOptions.coalesceSourceTs}
|
|
1252
|
+
* describes. Re-asking them would answer the same message twice.
|
|
1253
|
+
*
|
|
1254
|
+
* Items with no `sourceTs` cannot be matched and are kept.
|
|
1255
|
+
*/
|
|
1256
|
+
private async withoutNudgedFollowups(
|
|
1257
|
+
sessionId: string,
|
|
1258
|
+
queued: readonly CoalescedQueuedTurn[]
|
|
1259
|
+
): Promise<CoalescedQueuedTurn[]> {
|
|
1260
|
+
const record = await this.sessions.get(sessionId);
|
|
1261
|
+
const continuationKey = record?.continuationKey;
|
|
1262
|
+
if (continuationKey === undefined || continuationKey === null) {
|
|
1263
|
+
return [...queued];
|
|
1264
|
+
}
|
|
1265
|
+
const kept: CoalescedQueuedTurn[] = [];
|
|
1266
|
+
for (const item of queued) {
|
|
1267
|
+
if (item.sourceTs === undefined) {
|
|
1268
|
+
kept.push(item);
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
if (
|
|
1273
|
+
!(await this.nudgeStore.isDelivered(continuationKey, item.sourceTs))
|
|
1274
|
+
) {
|
|
1275
|
+
kept.push(item);
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
// A store read failure must not swallow the ask.
|
|
1279
|
+
kept.push(item);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return kept;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** Mark drained follow-ups delivered so the next turn's carrier skips them. */
|
|
1286
|
+
private async settleDrainedFollowups(
|
|
1287
|
+
sessionId: string,
|
|
1288
|
+
items: readonly CoalescedQueuedTurn[]
|
|
1289
|
+
): Promise<void> {
|
|
1290
|
+
const record = await this.sessions.get(sessionId);
|
|
1291
|
+
const continuationKey = record?.continuationKey;
|
|
1292
|
+
if (continuationKey === undefined || continuationKey === null) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
for (const item of items) {
|
|
1296
|
+
if (item.sourceTs === undefined) {
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
await this.nudgeStore.markDelivered(continuationKey, item.sourceTs);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
// Worst case the carrier re-injects it; the ask still gets answered.
|
|
1303
|
+
this.logger(
|
|
1304
|
+
`[session] nudge markDelivered failed for ${sessionId}: ${describeError(error)}`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1129
1310
|
private formatCoalescedFollowups(
|
|
1130
1311
|
items: readonly CoalescedQueuedTurn[]
|
|
1131
1312
|
): string {
|
|
@@ -1152,9 +1333,20 @@ export class SessionEngine {
|
|
|
1152
1333
|
return;
|
|
1153
1334
|
}
|
|
1154
1335
|
this.coalesceQueues.delete(sessionId);
|
|
1155
|
-
const
|
|
1336
|
+
const pending = await this.withoutNudgedFollowups(sessionId, queued);
|
|
1337
|
+
if (pending.length === 0) {
|
|
1338
|
+
this.logger(
|
|
1339
|
+
`[session] coalesce drain skipped; all follow-ups already nudged ${sessionId}`
|
|
1340
|
+
);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
// These become the next turn's prompt, so settle them in the nudge store
|
|
1344
|
+
// first: a surviving pending record would be injected by that turn's own
|
|
1345
|
+
// carrier, re-asking what the turn is already about.
|
|
1346
|
+
await this.settleDrainedFollowups(sessionId, pending);
|
|
1347
|
+
const message = this.formatCoalescedFollowups(pending);
|
|
1156
1348
|
this.logger(
|
|
1157
|
-
`[session] coalesce drain starting follow-up (${
|
|
1349
|
+
`[session] coalesce drain starting follow-up (${pending.length}) ${sessionId}`
|
|
1158
1350
|
);
|
|
1159
1351
|
await this.startTurn(sessionId, message, {
|
|
1160
1352
|
admission: "preempt",
|
|
@@ -1179,6 +1371,15 @@ export class SessionEngine {
|
|
|
1179
1371
|
return true;
|
|
1180
1372
|
}
|
|
1181
1373
|
|
|
1374
|
+
// Another server owns this session, so coalesce even a preempt. No local
|
|
1375
|
+
// enqueue: `drainCoalescedFollowups` reads only this process's queue, so
|
|
1376
|
+
// the shared nudge store is the carrier. Not redundant with `acquire`
|
|
1377
|
+
// below — this returns before preempt's `clearCoalesced` drops the queue.
|
|
1378
|
+
if (await this.turnLeases.runningElsewhere(sessionId)) {
|
|
1379
|
+
this.logger(`[session] coalesce; another server owns ${sessionId}`);
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1182
1383
|
// Interrupt any turn in flight and wait for it to settle so the SDK run is
|
|
1183
1384
|
// terminal before this turn's send (the follow-up also force-expires as a
|
|
1184
1385
|
// belt-and-suspenders — see buildTurnRequest). Drain-resume skips interrupt:
|
|
@@ -1192,8 +1393,20 @@ export class SessionEngine {
|
|
|
1192
1393
|
if (admission === "preempt" && options.retainCoalesced !== true) {
|
|
1193
1394
|
this.clearCoalesced(sessionId);
|
|
1194
1395
|
}
|
|
1396
|
+
// A neighbour won the race; same outcome as the branch above.
|
|
1397
|
+
const lease = await this.turnLeases.acquire(sessionId, () => {
|
|
1398
|
+
// Stalled past the stale window and a successor took over. Stand down
|
|
1399
|
+
// rather than answer the thread alongside it.
|
|
1400
|
+
this.logger(`[session] lease lost mid-turn; aborting ${sessionId}`);
|
|
1401
|
+
this.turnAborts.get(sessionId)?.abort();
|
|
1402
|
+
});
|
|
1403
|
+
if (lease === undefined) {
|
|
1404
|
+
this.logger(`[session] lease lost during admit ${sessionId}`);
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1195
1407
|
this.pendingTurns.add(sessionId);
|
|
1196
1408
|
|
|
1409
|
+
let lifecycleInstalled = false;
|
|
1197
1410
|
try {
|
|
1198
1411
|
let record = await this.updateRecord(sessionId, (current) => ({
|
|
1199
1412
|
...current,
|
|
@@ -1259,11 +1472,20 @@ export class SessionEngine {
|
|
|
1259
1472
|
}
|
|
1260
1473
|
}
|
|
1261
1474
|
}
|
|
1475
|
+
// The drain may have nested a successor turn, which took its own
|
|
1476
|
+
// reference; the lease outlives this release until that one ends too.
|
|
1477
|
+
await lease.release();
|
|
1262
1478
|
return outcome;
|
|
1263
1479
|
})();
|
|
1264
1480
|
this.activeTurns.set(sessionId, lifecycle);
|
|
1481
|
+
lifecycleInstalled = true;
|
|
1265
1482
|
} finally {
|
|
1266
1483
|
this.pendingTurns.delete(sessionId);
|
|
1484
|
+
if (!lifecycleInstalled) {
|
|
1485
|
+
// Nothing will run the release above, and the session would sit behind
|
|
1486
|
+
// this lease until it went stale.
|
|
1487
|
+
await lease.release();
|
|
1488
|
+
}
|
|
1267
1489
|
}
|
|
1268
1490
|
return false;
|
|
1269
1491
|
}
|
|
@@ -1435,7 +1657,13 @@ export class SessionEngine {
|
|
|
1435
1657
|
...(agent.allowedTools === undefined
|
|
1436
1658
|
? {}
|
|
1437
1659
|
: { tools: agent.allowedTools }),
|
|
1660
|
+
// No session yet, so auth'd connections are omitted rather than
|
|
1661
|
+
// warmed without an identity; the per-send flag still has to match
|
|
1662
|
+
// the turns' requests or the warmed executor is one no turn asks for.
|
|
1438
1663
|
mcpServers: this.buildMcpServers("local"),
|
|
1664
|
+
...(this.hasAttachedAuthedConnections()
|
|
1665
|
+
? { perSendMcpServers: true }
|
|
1666
|
+
: {}),
|
|
1439
1667
|
subagents: buildRunnerSubagents(agent),
|
|
1440
1668
|
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
1441
1669
|
});
|
|
@@ -1521,6 +1749,18 @@ export class SessionEngine {
|
|
|
1521
1749
|
seed: isFirstTurn,
|
|
1522
1750
|
stateRoot: this.stateRoot,
|
|
1523
1751
|
});
|
|
1752
|
+
// Must precede the carrier: the harness resolves rules, skills,
|
|
1753
|
+
// `mcp.json` and hooks from its git root, so a workspace nested in an
|
|
1754
|
+
// outer repo would load that repo's hooks instead of the ones written
|
|
1755
|
+
// just below. Only for directories this session owns — a borrowed
|
|
1756
|
+
// checkout or worktree keeps its own root.
|
|
1757
|
+
if (!this.hasBorrowedWorkspace(record)) {
|
|
1758
|
+
await ensureOwnGitRoot(harnessCwd);
|
|
1759
|
+
}
|
|
1760
|
+
await this.materializeNudgeCarrier(record, harnessCwd);
|
|
1761
|
+
// Baseline before the agent can run, so a follow-up that arrives and
|
|
1762
|
+
// drains before the first tool boundary still counts as an increase.
|
|
1763
|
+
await this.baselineDeliveredNudges(record);
|
|
1524
1764
|
const pendingFiles = record.pendingWorkspaceFiles;
|
|
1525
1765
|
const pendingImages = record.pendingImages;
|
|
1526
1766
|
if (
|
|
@@ -1555,7 +1795,7 @@ export class SessionEngine {
|
|
|
1555
1795
|
isFirstTurn &&
|
|
1556
1796
|
(this.hasBorrowedWorkspace(record) ||
|
|
1557
1797
|
(await isNestedInGitRepo(harnessCwd)));
|
|
1558
|
-
const request = this.buildTurnRequest(
|
|
1798
|
+
const request = await this.buildTurnRequest(
|
|
1559
1799
|
record,
|
|
1560
1800
|
agent,
|
|
1561
1801
|
turnId,
|
|
@@ -1574,6 +1814,9 @@ export class SessionEngine {
|
|
|
1574
1814
|
// typecheck green against older published `@cursor/sdk` pins.
|
|
1575
1815
|
(update as { type: string }).type === "tool-call-delta"
|
|
1576
1816
|
) {
|
|
1817
|
+
// Same boundary the carrier hook runs on, so a drain is visible
|
|
1818
|
+
// by the time this fires.
|
|
1819
|
+
await this.emitDeliveredNudges(record, emit);
|
|
1577
1820
|
await yieldEventLoop();
|
|
1578
1821
|
}
|
|
1579
1822
|
},
|
|
@@ -1593,6 +1836,9 @@ export class SessionEngine {
|
|
|
1593
1836
|
permit.release();
|
|
1594
1837
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1595
1838
|
await this.clearPendingApprovals(sessionId);
|
|
1839
|
+
// Anything still queued belongs to the next turn's boundary drain, not
|
|
1840
|
+
// to this turn's edge detection.
|
|
1841
|
+
this.deliveredNudges.delete(sessionId);
|
|
1596
1842
|
}
|
|
1597
1843
|
}
|
|
1598
1844
|
|
|
@@ -1672,7 +1918,7 @@ export class SessionEngine {
|
|
|
1672
1918
|
};
|
|
1673
1919
|
}
|
|
1674
1920
|
|
|
1675
|
-
private buildTurnRequest(
|
|
1921
|
+
private async buildTurnRequest(
|
|
1676
1922
|
record: SessionRecord,
|
|
1677
1923
|
agent: ResolvedAgent,
|
|
1678
1924
|
turnId: string,
|
|
@@ -1683,7 +1929,7 @@ export class SessionEngine {
|
|
|
1683
1929
|
onUpdate: RunnerTurnRequest["onUpdate"],
|
|
1684
1930
|
signal: AbortSignal,
|
|
1685
1931
|
images?: Array<{ data: string; mimeType: string }>
|
|
1686
|
-
): RunnerTurnRequest {
|
|
1932
|
+
): Promise<RunnerTurnRequest> {
|
|
1687
1933
|
// Per-session cloud attach (channel/tool handoff) forces cloud runtime
|
|
1688
1934
|
// even when the agent defaults to local for chat server tools.
|
|
1689
1935
|
const runtime = resolveSessionRuntime({
|
|
@@ -1699,6 +1945,21 @@ export class SessionEngine {
|
|
|
1699
1945
|
`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.`
|
|
1700
1946
|
);
|
|
1701
1947
|
}
|
|
1948
|
+
// Same fail-closed posture for per-session connection features:
|
|
1949
|
+
// advertised tools are server tools (in-process, local turns only) and
|
|
1950
|
+
// per-session auth headers cannot reach a cloud VM's MCP config. A turn
|
|
1951
|
+
// that silently runs without its per-session tools or identity is worse
|
|
1952
|
+
// than a refused one. Cloud-runtime agents already fail discovery; this
|
|
1953
|
+
// guards hybrid agents' per-send cloud sessions.
|
|
1954
|
+
const perSessionConnection = agent.connections.find(
|
|
1955
|
+
(connection) =>
|
|
1956
|
+
connection.advertiseTools === true || connection.auth !== undefined
|
|
1957
|
+
);
|
|
1958
|
+
if (runtime === "cloud" && perSessionConnection !== undefined) {
|
|
1959
|
+
throw new Error(
|
|
1960
|
+
`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.`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1702
1963
|
const mergedCloud =
|
|
1703
1964
|
runtime === "cloud"
|
|
1704
1965
|
? mergeCloudOptions({
|
|
@@ -1764,14 +2025,26 @@ export class SessionEngine {
|
|
|
1764
2025
|
runtime,
|
|
1765
2026
|
...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
|
|
1766
2027
|
customTools:
|
|
1767
|
-
runtime === "local"
|
|
1768
|
-
|
|
2028
|
+
runtime === "local"
|
|
2029
|
+
? await this.buildCustomTools(record, agent, turnId, emit)
|
|
2030
|
+
: {},
|
|
2031
|
+
mcpServers: this.buildMcpServers(
|
|
2032
|
+
runtime,
|
|
2033
|
+
record.sessionId,
|
|
2034
|
+
await this.evaluateConnectionAuth(record)
|
|
2035
|
+
),
|
|
2036
|
+
...(runtime === "local" && this.hasAttachedAuthedConnections()
|
|
2037
|
+
? { perSendMcpServers: true }
|
|
2038
|
+
: {}),
|
|
1769
2039
|
subagents: buildRunnerSubagents(agent),
|
|
1770
2040
|
onUpdate,
|
|
1771
2041
|
signal,
|
|
1772
2042
|
// Local follow-ups expire any lingering active run so a preempted turn
|
|
1773
2043
|
// (or one orphaned by a crashed process) never blocks this send.
|
|
1774
2044
|
forceExpireActiveRun: runtime === "local" && !isFirstTurn,
|
|
2045
|
+
onRequestId: (requestId) => {
|
|
2046
|
+
emit({ type: "turn.bound", data: { requestId } });
|
|
2047
|
+
},
|
|
1775
2048
|
onSdkAgentId: async (sdkAgentId) => {
|
|
1776
2049
|
if (record.sdkAgentId === sdkAgentId) {
|
|
1777
2050
|
return;
|
|
@@ -1871,11 +2144,18 @@ export class SessionEngine {
|
|
|
1871
2144
|
*/
|
|
1872
2145
|
private buildMcpServers(
|
|
1873
2146
|
runtime: "local" | "cloud",
|
|
1874
|
-
sessionId?: string
|
|
2147
|
+
sessionId?: string,
|
|
2148
|
+
sessionAuth?: ReadonlyMap<string, Record<string, string>>
|
|
1875
2149
|
): Record<string, ConnectionTransport> {
|
|
2150
|
+
// Advertised connections surface as named server tools instead of an
|
|
2151
|
+
// attached MCP server (each tool appears exactly once), so they are
|
|
2152
|
+
// excluded here alongside hostOnly.
|
|
1876
2153
|
const hostOnlyNames = new Set(
|
|
1877
2154
|
this.project.agent.connections
|
|
1878
|
-
.filter(
|
|
2155
|
+
.filter(
|
|
2156
|
+
(connection) =>
|
|
2157
|
+
connection.hostOnly === true || connection.advertiseTools === true
|
|
2158
|
+
)
|
|
1879
2159
|
.map((connection) => connection.name)
|
|
1880
2160
|
);
|
|
1881
2161
|
const bridgedNames = new Set(
|
|
@@ -1920,6 +2200,23 @@ export class SessionEngine {
|
|
|
1920
2200
|
}
|
|
1921
2201
|
continue;
|
|
1922
2202
|
}
|
|
2203
|
+
// Per-session auth: merge the evaluated headers over the static
|
|
2204
|
+
// ones. With no evaluation in hand (prewarm has no session) the
|
|
2205
|
+
// connection is omitted rather than attached without its identity.
|
|
2206
|
+
if (connection.auth !== undefined) {
|
|
2207
|
+
const evaluated = sessionAuth?.get(connection.name);
|
|
2208
|
+
if (evaluated === undefined) {
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const transport = connection.transport;
|
|
2212
|
+
if ("url" in transport) {
|
|
2213
|
+
servers[connection.name] = {
|
|
2214
|
+
...transport,
|
|
2215
|
+
headers: { ...transport.headers, ...evaluated },
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
1923
2220
|
servers[connection.name] = connection.transport;
|
|
1924
2221
|
}
|
|
1925
2222
|
for (const resolved of this.resolvedConnections) {
|
|
@@ -1966,20 +2263,67 @@ export class SessionEngine {
|
|
|
1966
2263
|
return servers;
|
|
1967
2264
|
}
|
|
1968
2265
|
|
|
1969
|
-
|
|
2266
|
+
/**
|
|
2267
|
+
* True when a connection with per-session `auth` attaches to the model
|
|
2268
|
+
* (not `hostOnly`, not advertised). These turns set
|
|
2269
|
+
* {@link RunnerTurnRequest.perSendMcpServers} so the evaluated headers
|
|
2270
|
+
* ride every send instead of being pinned on the cached agent handle.
|
|
2271
|
+
*/
|
|
2272
|
+
private hasAttachedAuthedConnections(): boolean {
|
|
2273
|
+
return this.project.agent.connections.some(
|
|
2274
|
+
(connection) =>
|
|
2275
|
+
connection.auth !== undefined &&
|
|
2276
|
+
connection.hostOnly !== true &&
|
|
2277
|
+
connection.advertiseTools !== true
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Evaluate per-session connection auth (`defineConnection({ auth })`)
|
|
2283
|
+
* for connections that attach to the model this turn. Advertised
|
|
2284
|
+
* connections evaluate their own auth inside the tool synthesis path. A
|
|
2285
|
+
* callback that throws fails the turn — a turn never silently runs
|
|
2286
|
+
* without the connection's identity.
|
|
2287
|
+
*/
|
|
2288
|
+
private async evaluateConnectionAuth(
|
|
2289
|
+
record: SessionRecord
|
|
2290
|
+
): Promise<ReadonlyMap<string, Record<string, string>> | undefined> {
|
|
2291
|
+
const authed = this.project.agent.connections.filter(
|
|
2292
|
+
(connection) =>
|
|
2293
|
+
connection.auth !== undefined &&
|
|
2294
|
+
connection.hostOnly !== true &&
|
|
2295
|
+
connection.advertiseTools !== true
|
|
2296
|
+
);
|
|
2297
|
+
if (authed.length === 0) {
|
|
2298
|
+
return undefined;
|
|
2299
|
+
}
|
|
2300
|
+
const session = this.sessionInfo(record);
|
|
2301
|
+
const evaluated = new Map<string, Record<string, string>>();
|
|
2302
|
+
for (const connection of authed) {
|
|
2303
|
+
const auth = connection.auth;
|
|
2304
|
+
if (auth === undefined) {
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
try {
|
|
2308
|
+
evaluated.set(connection.name, (await auth(session)).headers);
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
throw new Error(
|
|
2311
|
+
`Connection "${connection.name}" auth failed: ${describeError(error)}`,
|
|
2312
|
+
{ cause: error }
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return evaluated;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
private async buildCustomTools(
|
|
1970
2320
|
record: SessionRecord,
|
|
2321
|
+
agent: ResolvedAgent,
|
|
1971
2322
|
turnId: string,
|
|
1972
2323
|
emit: (payload: SessionEventPayload) => void
|
|
1973
|
-
): Record<string, SDKCustomTool
|
|
2324
|
+
): Promise<Record<string, SDKCustomTool>> {
|
|
1974
2325
|
const tools: Record<string, SDKCustomTool> = {};
|
|
1975
|
-
|
|
1976
|
-
if (tool.execution !== "server") {
|
|
1977
|
-
continue;
|
|
1978
|
-
}
|
|
1979
|
-
const execute = tool.definition.execute;
|
|
1980
|
-
if (typeof execute !== "function") {
|
|
1981
|
-
continue;
|
|
1982
|
-
}
|
|
2326
|
+
const register = (tool: ResolvedAgent["tools"][number]): void => {
|
|
1983
2327
|
tools[tool.name] = {
|
|
1984
2328
|
description: tool.description,
|
|
1985
2329
|
inputSchema: tool.inputSchema as SDKCustomTool["inputSchema"],
|
|
@@ -1994,6 +2338,26 @@ export class SessionEngine {
|
|
|
1994
2338
|
});
|
|
1995
2339
|
},
|
|
1996
2340
|
};
|
|
2341
|
+
};
|
|
2342
|
+
for (const tool of agent.tools) {
|
|
2343
|
+
if (tool.execution !== "server") {
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
if (typeof tool.definition.execute !== "function") {
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
register(tool);
|
|
2350
|
+
}
|
|
2351
|
+
// Advertised connections (`defineConnection({ advertiseTools })`):
|
|
2352
|
+
// named passthroughs resolved fresh on every turn — reminder fires and
|
|
2353
|
+
// post-restart follow-ups included — and a listing failure throws,
|
|
2354
|
+
// failing the turn closed.
|
|
2355
|
+
for (const tool of await resolveAdvertisedTools({
|
|
2356
|
+
agent,
|
|
2357
|
+
session: this.sessionInfo(record),
|
|
2358
|
+
hostMcp: this.mcpHost,
|
|
2359
|
+
})) {
|
|
2360
|
+
register(tool);
|
|
1997
2361
|
}
|
|
1998
2362
|
return tools;
|
|
1999
2363
|
}
|
|
@@ -3476,6 +3840,8 @@ export class SessionEngine {
|
|
|
3476
3840
|
await Promise.allSettled([...this.backgroundWork]);
|
|
3477
3841
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
3478
3842
|
await Promise.allSettled([...this.updateChains.values()]);
|
|
3843
|
+
// After the drain, so a successor need not wait out the heartbeat TTL.
|
|
3844
|
+
await this.turnLeases.releaseAll().catch(() => {});
|
|
3479
3845
|
await this.logs.flushAll();
|
|
3480
3846
|
await this.storage?.close();
|
|
3481
3847
|
await this.mcpHost.close();
|