@cursor/july 0.1.80 → 0.1.82
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 +0 -12
- package/dist/channels/github/cursor-account.d.ts +11 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +13 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +29 -1
- package/dist/channels/github/types.d.ts +8 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +32 -10
- package/dist/channels/origin/types.d.ts +7 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +2 -2
- package/dist/channels/slack/dispatch.d.ts +3 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +18 -18
- package/dist/channels/slack/interactive.d.ts +2 -0
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +4 -16
- package/dist/channels/slack/slack-channel.d.ts +1 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +21 -10
- package/dist/channels/slack/types.d.ts +7 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +4 -0
- package/dist/continuation.d.ts +20 -0
- package/dist/continuation.d.ts.map +1 -0
- package/dist/continuation.js +18 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.e4_wtjUS.js → app.BEHVaPKt.js} +1 -1
- package/dist/docs/assets/chunks/{@localSearchIndexroot.BGiMJpnL.js → @localSearchIndexroot.Vvio4stV.js} +1 -1
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DbdzyEie.js → VPLocalSearchBox.D2-5PKih.js} +1 -1
- package/dist/docs/assets/chunks/{theme.D8HpLrLK.js → theme.uNqJ4MUO.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/convert-automation.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/opentelemetry.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/artifacts.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/templates/agentic-owners.html +2 -2
- package/dist/docs/templates/demo.html +2 -2
- package/dist/docs/templates/pr-autofixer.html +2 -2
- package/dist/docs/templates/security-reviewer.html +2 -2
- package/dist/docs/templates/triage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/files.d.ts +9 -3
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +9 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/internal/continuation-channel.d.ts +29 -0
- package/dist/internal/continuation-channel.d.ts.map +1 -0
- package/dist/internal/continuation-channel.js +88 -0
- package/dist/internal/continuation-identity.d.ts +95 -0
- package/dist/internal/continuation-identity.d.ts.map +1 -0
- package/dist/internal/continuation-identity.js +445 -0
- package/dist/internal/deploy-manifest.d.ts +12 -2
- package/dist/internal/deploy-manifest.d.ts.map +1 -1
- package/dist/internal/deploy-manifest.js +15 -7
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +32 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +18 -8
- package/dist/internal/session-engine.d.ts +24 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +104 -34
- package/dist/internal/session-store.d.ts.map +1 -1
- package/dist/internal/session-store.js +6 -0
- package/dist/internal/skills-sync.d.ts +25 -0
- package/dist/internal/skills-sync.d.ts.map +1 -0
- package/dist/internal/skills-sync.js +96 -0
- package/dist/playground/assets/{index-DJzcIeGt.css → index-ChETdG-_.css} +1 -1
- package/dist/playground/assets/{index-K3Sxyx0I.js → index-CkuIrhg4.js} +3 -3
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/channels/github/cursor-account.ts +20 -0
- package/src/channels/github/github-channel.ts +52 -1
- package/src/channels/github/types.ts +8 -0
- package/src/channels/origin/origin-channel.ts +48 -10
- package/src/channels/origin/types.ts +7 -0
- package/src/channels/slack/cursor-account.ts +1 -0
- package/src/channels/slack/dispatch.ts +44 -2
- package/src/channels/slack/interactive.ts +5 -0
- package/src/channels/slack/slack-channel.ts +23 -2
- package/src/channels/slack/types.ts +7 -0
- package/src/channels.ts +7 -0
- package/src/continuation.ts +49 -0
- package/src/files.ts +10 -3
- package/src/index.ts +30 -1
- package/src/internal/continuation-channel.ts +129 -0
- package/src/internal/continuation-identity.ts +597 -0
- package/src/internal/deploy-manifest.ts +36 -3
- package/src/internal/discovery.ts +40 -2
- package/src/internal/server.ts +23 -9
- package/src/internal/session-engine.ts +132 -37
- package/src/internal/session-store.ts +6 -0
- package/src/internal/skills-sync.ts +107 -0
- package/src/types.ts +55 -0
|
@@ -23,6 +23,7 @@ import { httpChannel } from "../channels.js";
|
|
|
23
23
|
import type { OtelDefinition } from "../otel.js";
|
|
24
24
|
import type { StorageDefinition } from "../storage.js";
|
|
25
25
|
import {
|
|
26
|
+
type AgentArchitecture,
|
|
26
27
|
type AgentConcurrencyOptions,
|
|
27
28
|
type AgentConfigDefinition,
|
|
28
29
|
type AgentHostingOptions,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
builtinTools,
|
|
69
70
|
TAG_ARTIFACT_TOOL_NAME,
|
|
70
71
|
} from "./builtin-tools/index.js";
|
|
72
|
+
import { applyAgentContinuationDefaults } from "./continuation-channel.js";
|
|
71
73
|
import { isValidCron } from "./cron.js";
|
|
72
74
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
73
75
|
import {
|
|
@@ -311,12 +313,19 @@ export async function loadAgentProject(
|
|
|
311
313
|
await warnUnknownSlots(ctx, agentDir);
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
const resolvedHttpChannel = httpChannelDefinition ?? httpChannel();
|
|
317
|
+
applyAgentContinuationDefaults({
|
|
318
|
+
agent,
|
|
319
|
+
channels,
|
|
320
|
+
httpChannel: resolvedHttpChannel,
|
|
321
|
+
});
|
|
322
|
+
|
|
314
323
|
return {
|
|
315
324
|
rootDir,
|
|
316
325
|
agentDir,
|
|
317
326
|
name: agent.name,
|
|
318
327
|
agent,
|
|
319
|
-
httpChannel:
|
|
328
|
+
httpChannel: resolvedHttpChannel,
|
|
320
329
|
channels,
|
|
321
330
|
schedules,
|
|
322
331
|
hooks,
|
|
@@ -387,6 +396,13 @@ async function loadAgent(
|
|
|
387
396
|
"tools is not supported on subagents (they keep the harness task-tool toolset); ignoring.",
|
|
388
397
|
});
|
|
389
398
|
}
|
|
399
|
+
if (config?.architecture !== undefined) {
|
|
400
|
+
ctx.diagnostics.push({
|
|
401
|
+
severity: "warning",
|
|
402
|
+
path: displayPath(ctx, join(agentDir, "agent.ts")),
|
|
403
|
+
message: "architecture is not supported on subagents; ignoring.",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
390
406
|
const artifactsPath = await findModuleFile(agentDir, "artifacts");
|
|
391
407
|
if (artifactsPath !== undefined) {
|
|
392
408
|
ctx.diagnostics.push({
|
|
@@ -505,6 +521,7 @@ async function loadAgent(
|
|
|
505
521
|
};
|
|
506
522
|
const hosting = resolveHosting(ctx, agentDir, config?.hosting);
|
|
507
523
|
const concurrency = resolveConcurrency(ctx, agentDir, config?.concurrency);
|
|
524
|
+
const architecture = resolveArchitecture(ctx, agentDir, config?.architecture);
|
|
508
525
|
|
|
509
526
|
if (runtime === "cloud") {
|
|
510
527
|
warnCloudRuntimeLimitations(ctx, agentDir, {
|
|
@@ -529,6 +546,7 @@ async function loadAgent(
|
|
|
529
546
|
...(allowedTools === undefined ? {} : { allowedTools }),
|
|
530
547
|
hosting,
|
|
531
548
|
concurrency,
|
|
549
|
+
...(architecture === undefined ? {} : { architecture }),
|
|
532
550
|
tools,
|
|
533
551
|
skills,
|
|
534
552
|
connections,
|
|
@@ -537,6 +555,26 @@ async function loadAgent(
|
|
|
537
555
|
};
|
|
538
556
|
}
|
|
539
557
|
|
|
558
|
+
function resolveArchitecture(
|
|
559
|
+
ctx: DiscoveryContext,
|
|
560
|
+
agentDir: string,
|
|
561
|
+
architecture: AgentArchitecture | undefined
|
|
562
|
+
): AgentArchitecture | undefined {
|
|
563
|
+
if (
|
|
564
|
+
architecture === undefined ||
|
|
565
|
+
architecture === "v1" ||
|
|
566
|
+
architecture === "v2"
|
|
567
|
+
) {
|
|
568
|
+
return architecture;
|
|
569
|
+
}
|
|
570
|
+
ctx.diagnostics.push({
|
|
571
|
+
severity: "error",
|
|
572
|
+
path: displayPath(ctx, join(agentDir, "agent.ts")),
|
|
573
|
+
message: `architecture must be "v1" or "v2"; got ${JSON.stringify(architecture)}.`,
|
|
574
|
+
});
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
|
|
540
578
|
/** Absolute harness cwd parent, or undefined when unset / blank. */
|
|
541
579
|
function resolveAgentCwd(
|
|
542
580
|
ctx: DiscoveryContext,
|
|
@@ -1549,7 +1587,7 @@ function warnCloudRuntimeLimitations(
|
|
|
1549
1587
|
severity: "warning",
|
|
1550
1588
|
path: displayPath(ctx, join(agentDir, "skills")),
|
|
1551
1589
|
message:
|
|
1552
|
-
"Authored skills
|
|
1590
|
+
"Authored skills reach cloud agents on hosted deployments via the deployment store's skills/ directory (native skill discovery). Cloud turns started outside hosting (dev serve/run) see only skills already in the cloud repo.",
|
|
1553
1591
|
});
|
|
1554
1592
|
}
|
|
1555
1593
|
if (counts.seedFiles > 0) {
|
package/src/internal/server.ts
CHANGED
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
} from "../channels/deployments/deployments-channel.js";
|
|
21
21
|
import type { DeploymentsChannelOptions } from "../channels/deployments/types.js";
|
|
22
22
|
import {
|
|
23
|
+
type CursorAccountGithubTokenResolver,
|
|
23
24
|
type CursorGithubRepoScope,
|
|
24
25
|
createCursorAccountGithubTokenResolver,
|
|
26
|
+
createCursorGithubProxyHostResolver,
|
|
25
27
|
cursorGithubPermissionsForChannel,
|
|
26
28
|
type GithubCursorAccountRuntime,
|
|
27
29
|
getGithubCursorAccountRuntime,
|
|
@@ -316,6 +318,10 @@ export async function startServer(
|
|
|
316
318
|
: requireCursorEventsAuth(effectiveCursorEvents, accountKey);
|
|
317
319
|
|
|
318
320
|
let cursorGithubCredentials: CursorGithubCredentialProvider | undefined;
|
|
321
|
+
// Proxy-wired GitHub token resolver for `ctx.host.github` when no startup
|
|
322
|
+
// token mint runs (empty repo scope): host Octokit calls must route
|
|
323
|
+
// through github-proxy exactly like channel Octokit calls.
|
|
324
|
+
let hostGithubProxyResolver: CursorAccountGithubTokenResolver | undefined;
|
|
319
325
|
if (cursorGithubTargets.length > 0) {
|
|
320
326
|
const githubProxyRequested = options.cursorGithubProxy === true;
|
|
321
327
|
const needsGithubAuth =
|
|
@@ -408,23 +414,31 @@ export async function startServer(
|
|
|
408
414
|
);
|
|
409
415
|
target.accountRuntime.credentials.bindProxyTransport(proxyTransport);
|
|
410
416
|
}
|
|
417
|
+
// `ctx.host.github` gets the same proxy transport; without this it
|
|
418
|
+
// would fall back to ambient auth (env token / `gh`) or an
|
|
419
|
+
// unauthenticated Octokit, and every host GitHub call would 404.
|
|
420
|
+
hostGithubProxyResolver = createCursorGithubProxyHostResolver({
|
|
421
|
+
getApiKey: async () => cursorGithubAuth.apiKey,
|
|
422
|
+
transport: proxyTransport,
|
|
423
|
+
});
|
|
411
424
|
logger(
|
|
412
|
-
`[agent-sdk] GitHub auth: channel API calls route through ${githubProxyUrl} (repo scope per request)`
|
|
425
|
+
`[agent-sdk] GitHub auth: channel and host API calls route through ${githubProxyUrl} (repo scope per request)`
|
|
413
426
|
);
|
|
414
427
|
}
|
|
415
428
|
}
|
|
416
429
|
}
|
|
417
430
|
const resolvedCursorGithubCredentials = cursorGithubCredentials;
|
|
431
|
+
const hostGithubToken =
|
|
432
|
+
hostGithubProxyResolver ??
|
|
433
|
+
(resolvedCursorGithubCredentials === undefined
|
|
434
|
+
? undefined
|
|
435
|
+
: createCursorAccountGithubTokenResolver(() =>
|
|
436
|
+
resolvedCursorGithubCredentials.getToken()
|
|
437
|
+
));
|
|
418
438
|
const hostPlatforms = createHostPlatforms(
|
|
419
|
-
|
|
439
|
+
hostGithubToken === undefined
|
|
420
440
|
? {}
|
|
421
|
-
: {
|
|
422
|
-
githubCredentials: {
|
|
423
|
-
token: createCursorAccountGithubTokenResolver(() =>
|
|
424
|
-
resolvedCursorGithubCredentials.getToken()
|
|
425
|
-
),
|
|
426
|
-
},
|
|
427
|
-
}
|
|
441
|
+
: { githubCredentials: { token: hostGithubToken } }
|
|
428
442
|
);
|
|
429
443
|
|
|
430
444
|
if (playground) {
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
SlackNudgeStore,
|
|
25
25
|
} from "../channels/slack/nudge-store.js";
|
|
26
26
|
import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
cursorHostedFiles,
|
|
29
|
+
isCursorHostedFilesAvailable,
|
|
30
|
+
} from "../files-backends/cursor-hosted.js";
|
|
28
31
|
import {
|
|
29
32
|
type AgentProject,
|
|
30
33
|
type ApprovalDecision,
|
|
@@ -80,6 +83,16 @@ import {
|
|
|
80
83
|
isUnpricedCompletedTurn,
|
|
81
84
|
mapSdkTokenUsage,
|
|
82
85
|
} from "./cloud-turn-cost.js";
|
|
86
|
+
import {
|
|
87
|
+
applyAgentContinuationDefaults,
|
|
88
|
+
channelContinuationIdentity,
|
|
89
|
+
httpContinuationIdentity,
|
|
90
|
+
} from "./continuation-channel.js";
|
|
91
|
+
import {
|
|
92
|
+
continuationSource,
|
|
93
|
+
resolveV2Key,
|
|
94
|
+
V2EngineError,
|
|
95
|
+
} from "./continuation-identity.js";
|
|
83
96
|
import { isDevMode } from "./dev-mode.js";
|
|
84
97
|
import { type EventLogMemoryStats, EventLogStore } from "./event-log.js";
|
|
85
98
|
import { TurnEventMapper } from "./event-mapper.js";
|
|
@@ -113,6 +126,7 @@ import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections
|
|
|
113
126
|
import type { AgentRunner } from "./sdk-runner.js";
|
|
114
127
|
import { sessionExternalLinks } from "./session-links.js";
|
|
115
128
|
import { SessionStore } from "./session-store.js";
|
|
129
|
+
import { syncSkillsToStore } from "./skills-sync.js";
|
|
116
130
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
117
131
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
118
132
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
@@ -302,6 +316,8 @@ export class SessionEngine {
|
|
|
302
316
|
>();
|
|
303
317
|
/** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
|
|
304
318
|
private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
|
|
319
|
+
/** In-flight skills → deployment store sync (see ensureCloudSkillsSynced). */
|
|
320
|
+
private cloudSkillsSync: Promise<boolean> | undefined;
|
|
305
321
|
|
|
306
322
|
private readonly runner: AgentRunner;
|
|
307
323
|
private readonly logger: (line: string) => void;
|
|
@@ -366,6 +382,7 @@ export class SessionEngine {
|
|
|
366
382
|
|
|
367
383
|
constructor(options: SessionEngineOptions) {
|
|
368
384
|
this.project = options.project;
|
|
385
|
+
applyAgentContinuationDefaults(this.project);
|
|
369
386
|
this.stateRoot = options.stateRoot;
|
|
370
387
|
this.runner = options.runner;
|
|
371
388
|
this.logger = options.logger ?? (() => {});
|
|
@@ -734,26 +751,50 @@ export class SessionEngine {
|
|
|
734
751
|
* Start or resume a session on a channel. When `continuationToken`
|
|
735
752
|
* addresses an existing session this is a follow-up turn; otherwise a new
|
|
736
753
|
* session is created (keyed by the provided token, or a fresh one).
|
|
754
|
+
*
|
|
755
|
+
* v2 (`/v2` selectors) refuses `continuationToken` and looks
|
|
756
|
+
* up `(agentId, cKey)` from a mailbox stamp or structured claim.
|
|
737
757
|
*/
|
|
738
758
|
async send(
|
|
739
759
|
channelId: string,
|
|
740
760
|
message: string,
|
|
741
761
|
options: EngineSendOptions = {}
|
|
742
762
|
): Promise<ChannelSession> {
|
|
743
|
-
const
|
|
763
|
+
const identity =
|
|
764
|
+
channelId === HTTP_CHANNEL_ID
|
|
765
|
+
? httpContinuationIdentity(this.project.httpChannel)
|
|
766
|
+
: channelContinuationIdentity(this.channelsById.get(channelId));
|
|
767
|
+
let sendOptions = options;
|
|
768
|
+
let skipContinuationLookup = false;
|
|
769
|
+
if (continuationSource(identity) === "control-plane") {
|
|
770
|
+
if (options.continuationToken !== undefined) {
|
|
771
|
+
throw new V2EngineError(
|
|
772
|
+
"v2: do not pass continuationToken; use mailboxContinuation or claim"
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const cKey = resolveV2Key(identity, options);
|
|
776
|
+
sendOptions = { ...options, continuationToken: cKey };
|
|
777
|
+
skipContinuationLookup = cKey.startsWith("evt:");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const auth = sendOptions.auth ?? null;
|
|
744
781
|
let existing =
|
|
745
|
-
|
|
782
|
+
skipContinuationLookup || sendOptions.continuationToken === undefined
|
|
746
783
|
? undefined
|
|
747
784
|
: await this.sessions.findByContinuation(
|
|
748
785
|
channelId,
|
|
749
|
-
|
|
786
|
+
sendOptions.continuationToken
|
|
750
787
|
);
|
|
751
|
-
if (
|
|
788
|
+
if (
|
|
789
|
+
existing === undefined &&
|
|
790
|
+
!skipContinuationLookup &&
|
|
791
|
+
sendOptions.continuationToken !== undefined
|
|
792
|
+
) {
|
|
752
793
|
// Local miss: the session may live only in the storage sink (it
|
|
753
794
|
// fell outside the startup restore window, or restore is lazy-only).
|
|
754
795
|
existing = await this.restoreSessionByContinuation(
|
|
755
796
|
channelId,
|
|
756
|
-
|
|
797
|
+
sendOptions.continuationToken
|
|
757
798
|
);
|
|
758
799
|
}
|
|
759
800
|
|
|
@@ -765,25 +806,25 @@ export class SessionEngine {
|
|
|
765
806
|
// synchronize). Leave the durable continuation key unchanged.
|
|
766
807
|
let record = existing;
|
|
767
808
|
const refreshWorkspace =
|
|
768
|
-
|
|
769
|
-
|
|
809
|
+
sendOptions.workspaceDir !== undefined &&
|
|
810
|
+
sendOptions.workspaceDir !== existing.workspaceOverride;
|
|
770
811
|
if (
|
|
771
812
|
refreshWorkspace ||
|
|
772
|
-
|
|
773
|
-
|
|
813
|
+
sendOptions.workspaceFiles !== undefined ||
|
|
814
|
+
sendOptions.images !== undefined
|
|
774
815
|
) {
|
|
775
816
|
record = await this.updateRecord(existing.sessionId, (current) => ({
|
|
776
817
|
...current,
|
|
777
818
|
workspaceOverride: refreshWorkspace
|
|
778
|
-
?
|
|
819
|
+
? sendOptions.workspaceDir
|
|
779
820
|
: current.workspaceOverride,
|
|
780
821
|
pendingWorkspaceFiles: mergePendingWorkspaceFiles(
|
|
781
822
|
current.pendingWorkspaceFiles,
|
|
782
|
-
|
|
823
|
+
sendOptions.workspaceFiles
|
|
783
824
|
),
|
|
784
825
|
pendingImages: mergePendingImages(
|
|
785
826
|
current.pendingImages,
|
|
786
|
-
|
|
827
|
+
sendOptions.images
|
|
787
828
|
),
|
|
788
829
|
}));
|
|
789
830
|
}
|
|
@@ -792,31 +833,32 @@ export class SessionEngine {
|
|
|
792
833
|
// and the first send's snapshot must not pin this session's turns to a
|
|
793
834
|
// stale repo/prUrl for the session's lifetime.
|
|
794
835
|
if (
|
|
795
|
-
|
|
796
|
-
JSON.stringify(
|
|
836
|
+
sendOptions.cloud !== undefined &&
|
|
837
|
+
JSON.stringify(sendOptions.cloud) !==
|
|
838
|
+
JSON.stringify(record.cloudOverride)
|
|
797
839
|
) {
|
|
798
840
|
record = await this.updateRecord(record.sessionId, (current) => ({
|
|
799
841
|
...current,
|
|
800
|
-
cloudOverride:
|
|
842
|
+
cloudOverride: sendOptions.cloud,
|
|
801
843
|
}));
|
|
802
844
|
}
|
|
803
845
|
// Refresh wake-derived channel state (GitHub headSha / refs) without
|
|
804
846
|
// wiping sticky fields. Null patch values are skipped.
|
|
805
|
-
if (
|
|
847
|
+
if (sendOptions.refreshState !== undefined) {
|
|
806
848
|
record = await this.updateRecord(record.sessionId, (current) => ({
|
|
807
849
|
...current,
|
|
808
850
|
channelState: mergeChannelStateSkipNull(
|
|
809
851
|
current.channelState,
|
|
810
|
-
|
|
852
|
+
sendOptions.refreshState
|
|
811
853
|
),
|
|
812
854
|
}));
|
|
813
855
|
}
|
|
814
856
|
const since = (await this.logs.get(record.sessionId)).count;
|
|
815
857
|
const coalesced = await this.startTurn(record.sessionId, message, {
|
|
816
|
-
admission:
|
|
817
|
-
...(
|
|
858
|
+
admission: sendOptions.admission ?? "preempt",
|
|
859
|
+
...(sendOptions.coalesceSourceTs === undefined
|
|
818
860
|
? {}
|
|
819
|
-
: { sourceTs:
|
|
861
|
+
: { sourceTs: sendOptions.coalesceSourceTs }),
|
|
820
862
|
});
|
|
821
863
|
return this.channelSession(
|
|
822
864
|
record.sessionId,
|
|
@@ -828,22 +870,22 @@ export class SessionEngine {
|
|
|
828
870
|
}
|
|
829
871
|
|
|
830
872
|
const now = new Date().toISOString();
|
|
831
|
-
const channelState = this.initialChannelState(channelId,
|
|
832
|
-
const purpose =
|
|
873
|
+
const channelState = this.initialChannelState(channelId, sendOptions.state);
|
|
874
|
+
const purpose = sendOptions.purpose === "eval" ? "eval" : "live";
|
|
833
875
|
let record: SessionRecord = {
|
|
834
876
|
sessionId: newSessionId(),
|
|
835
877
|
channelId,
|
|
836
|
-
mode:
|
|
878
|
+
mode: sendOptions.mode ?? "chat",
|
|
837
879
|
purpose,
|
|
838
|
-
continuationKey:
|
|
880
|
+
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
839
881
|
auth,
|
|
840
|
-
title:
|
|
882
|
+
title: sendOptions.title,
|
|
841
883
|
channelState,
|
|
842
|
-
workspaceOverride:
|
|
843
|
-
pendingWorkspaceFiles:
|
|
844
|
-
pendingImages:
|
|
845
|
-
cloudOverride:
|
|
846
|
-
sdkAgentId:
|
|
884
|
+
workspaceOverride: sendOptions.workspaceDir,
|
|
885
|
+
pendingWorkspaceFiles: sendOptions.workspaceFiles,
|
|
886
|
+
pendingImages: sendOptions.images,
|
|
887
|
+
cloudOverride: sendOptions.cloud,
|
|
888
|
+
sdkAgentId: sendOptions.sdkAgentId,
|
|
847
889
|
turnCount: 0,
|
|
848
890
|
createdAt: now,
|
|
849
891
|
updatedAt: now,
|
|
@@ -857,10 +899,10 @@ export class SessionEngine {
|
|
|
857
899
|
record = await this.enrollABs(record);
|
|
858
900
|
const since = (await this.logs.get(record.sessionId)).count;
|
|
859
901
|
await this.startTurn(record.sessionId, message, {
|
|
860
|
-
admission:
|
|
861
|
-
...(
|
|
902
|
+
admission: sendOptions.admission ?? "preempt",
|
|
903
|
+
...(sendOptions.coalesceSourceTs === undefined
|
|
862
904
|
? {}
|
|
863
|
-
: { sourceTs:
|
|
905
|
+
: { sourceTs: sendOptions.coalesceSourceTs }),
|
|
864
906
|
});
|
|
865
907
|
return this.channelSession(record.sessionId, record, true, false, since);
|
|
866
908
|
}
|
|
@@ -1753,6 +1795,51 @@ export class SessionEngine {
|
|
|
1753
1795
|
};
|
|
1754
1796
|
}
|
|
1755
1797
|
|
|
1798
|
+
/**
|
|
1799
|
+
* Sync authored skills onto the deployment store at `skills/<name>/…`
|
|
1800
|
+
* before the first cloud prompt, so `--agent-store-skills-dir` and the
|
|
1801
|
+
* first-turn S3 scan can discover them. False when the sync fails: the
|
|
1802
|
+
* turn then runs without store skills rather than pointing discovery at
|
|
1803
|
+
* paths that do not exist.
|
|
1804
|
+
*
|
|
1805
|
+
* With skills authored, every first cloud turn re-copies the authored
|
|
1806
|
+
* bytes and the memo only dedupes concurrent first turns. The mount is
|
|
1807
|
+
* writable from cloud sessions, so a once-per-process copy would let one
|
|
1808
|
+
* session's edits masquerade as agent definition for every later session
|
|
1809
|
+
* until the engine restarts; re-syncing keeps the store true to discovery
|
|
1810
|
+
* the same way local materialization always rewrites skills. Without
|
|
1811
|
+
* skills the sync is best-effort stale cleanup and one attempt per
|
|
1812
|
+
* process is enough, success or not — a deployment without the files
|
|
1813
|
+
* surface must not fail and log on every new session for a feature it
|
|
1814
|
+
* does not use.
|
|
1815
|
+
*/
|
|
1816
|
+
private ensureCloudSkillsSynced(): Promise<boolean> {
|
|
1817
|
+
if (this.cloudSkillsSync !== undefined) {
|
|
1818
|
+
return this.cloudSkillsSync;
|
|
1819
|
+
}
|
|
1820
|
+
const skills = this.project.agent.skills;
|
|
1821
|
+
let sync = syncSkillsToStore({
|
|
1822
|
+
sink: cursorHostedFiles(),
|
|
1823
|
+
agentName: this.project.name,
|
|
1824
|
+
skills,
|
|
1825
|
+
}).then(
|
|
1826
|
+
() => true,
|
|
1827
|
+
(error) => {
|
|
1828
|
+
this.logger(
|
|
1829
|
+
`[skills] deployment store sync failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1830
|
+
);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
);
|
|
1834
|
+
if (skills.length > 0) {
|
|
1835
|
+
sync = sync.finally(() => {
|
|
1836
|
+
this.cloudSkillsSync = undefined;
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
this.cloudSkillsSync = sync;
|
|
1840
|
+
return sync;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1756
1843
|
private async buildTurnRequest(
|
|
1757
1844
|
record: SessionRecord,
|
|
1758
1845
|
agent: ResolvedAgent,
|
|
@@ -1815,8 +1902,9 @@ export class SessionEngine {
|
|
|
1815
1902
|
if (isFirstTurn) {
|
|
1816
1903
|
if (runtime === "cloud") {
|
|
1817
1904
|
// Cloud VMs have no materialized workspace: ship the full context,
|
|
1818
|
-
// including agent tool script bodies. On hosted deployments
|
|
1819
|
-
//
|
|
1905
|
+
// including agent tool script bodies. On hosted deployments authored
|
|
1906
|
+
// skills land on the store mount for native discovery, and the
|
|
1907
|
+
// shared memory journal is on that mount too — say where. When the
|
|
1820
1908
|
// turn carries the session-bound server-tool MCP endpoint, the
|
|
1821
1909
|
// preamble catalogs server tools under that MCP server name *before*
|
|
1822
1910
|
// the agent-tool "write the script if missing" recipe, so a model
|
|
@@ -1828,7 +1916,14 @@ export class SessionEngine {
|
|
|
1828
1916
|
? undefined
|
|
1829
1917
|
: SERVER_TOOLS_MCP_SERVER_NAME,
|
|
1830
1918
|
});
|
|
1831
|
-
const
|
|
1919
|
+
const hosted = isCursorHostedFilesAvailable();
|
|
1920
|
+
// Synced whenever hosted, not just when skills exist: removing the
|
|
1921
|
+
// last skill has to delete its store copies too. Discovery is native
|
|
1922
|
+
// (`--agent-store-skills-dir`); this only has to land the bytes.
|
|
1923
|
+
if (hosted) {
|
|
1924
|
+
await this.ensureCloudSkillsSynced();
|
|
1925
|
+
}
|
|
1926
|
+
const memorySection = hosted
|
|
1832
1927
|
? `\n\n${buildCloudMemorySection(this.project.name)}`
|
|
1833
1928
|
: "";
|
|
1834
1929
|
turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
|
|
@@ -11,6 +11,7 @@ import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { SessionRecord } from "../types.js";
|
|
13
13
|
import { isSafeSessionId } from "./auth.js";
|
|
14
|
+
import { isV2DurableContinuationKey } from "./continuation-identity.js";
|
|
14
15
|
|
|
15
16
|
export class SessionStore {
|
|
16
17
|
private readonly cache = new Map<string, SessionRecord>();
|
|
@@ -39,6 +40,11 @@ export class SessionStore {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
private continuationKeyOf(channelId: string, key: string): string {
|
|
43
|
+
// v2 index is (agentId, cKey). This store is already per-agent, so drop
|
|
44
|
+
// channelId for `cont:` / `evt:` keys. v1 stays `{channelId}\n{token}`.
|
|
45
|
+
if (isV2DurableContinuationKey(key)) {
|
|
46
|
+
return `\0v2\n${key}`;
|
|
47
|
+
}
|
|
42
48
|
return `${channelId}\n${key}`;
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync authored skills into the deployment's Agent Store so cloud-runtime
|
|
3
|
+
* turns discover them natively. The engine writes through the hosted sink
|
|
4
|
+
* at store-root `skills/<name>/…`; the cloud VM is launched with
|
|
5
|
+
* `--agent-store-skills-dir /cursor/stores/deployment/skills` and walks that
|
|
6
|
+
* directory the same way it walks a user store.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { posix } from "node:path";
|
|
11
|
+
import { agentStoreKeys, FileConflictError, type FileSink } from "../files.js";
|
|
12
|
+
import { STORAGE_KEY_ROOT } from "../storage.js";
|
|
13
|
+
import type { DiscoveredSkill, SkillFileSource } from "../types.js";
|
|
14
|
+
import { buildSkillMd } from "./workspace.js";
|
|
15
|
+
|
|
16
|
+
/** Pre-native layout. Deleted on every sync so a rolled-forward deploy does not leave undiscoverable copies. */
|
|
17
|
+
function legacySkillsPrefix(agent: string): string {
|
|
18
|
+
return `${STORAGE_KEY_ROOT}/${agent}/skills/`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mirror the agent's skills onto the store: write every SKILL.md (with the
|
|
23
|
+
* same generated frontmatter local materialization uses) plus sibling files,
|
|
24
|
+
* and delete keys no authored skill claims so redeploys drop removed skills.
|
|
25
|
+
*
|
|
26
|
+
* Everything loads and validates before the store is touched, and writes
|
|
27
|
+
* land before stale deletes, so a failed sync never leaves a gap a
|
|
28
|
+
* concurrent cloud reader could see.
|
|
29
|
+
*/
|
|
30
|
+
export async function syncSkillsToStore(args: {
|
|
31
|
+
sink: FileSink;
|
|
32
|
+
/** Sweeps leftover keys from the pre-native `agentkit/v1/{agent}/skills/` prefix. */
|
|
33
|
+
agentName: string;
|
|
34
|
+
skills: readonly DiscoveredSkill[];
|
|
35
|
+
}): Promise<void> {
|
|
36
|
+
const desired = new Map<string, Uint8Array>();
|
|
37
|
+
for (const skill of args.skills) {
|
|
38
|
+
const key = (relPath: string): string =>
|
|
39
|
+
agentStoreKeys.skillFile(skill.name, relPath);
|
|
40
|
+
desired.set(key("SKILL.md"), Buffer.from(buildSkillMd(skill), "utf8"));
|
|
41
|
+
for (const [relPath, source] of Object.entries(skill.files)) {
|
|
42
|
+
desired.set(key(storeRelPath(relPath)), await sourceBytes(source));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await Promise.all(
|
|
46
|
+
[...desired].map(([key, body]) => putLastWriterWins(args.sink, key, body))
|
|
47
|
+
);
|
|
48
|
+
const [existing, leftover] = await Promise.all([
|
|
49
|
+
args.sink.list(agentStoreKeys.skillsPrefix()),
|
|
50
|
+
args.sink.list(legacySkillsPrefix(args.agentName)),
|
|
51
|
+
]);
|
|
52
|
+
await Promise.all(
|
|
53
|
+
[...existing.filter((entry) => !desired.has(entry.key)), ...leftover].map(
|
|
54
|
+
(entry) => args.sink.delete(entry.key)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Same refusals as local materialization's `resolveInside`: a rel that
|
|
61
|
+
* escapes the skill directory or would not round-trip through the FUSE
|
|
62
|
+
* mount (`..`, `.`, `//`, absolute) fails the sync, exactly as it fails a
|
|
63
|
+
* local turn's workspace write. Backslashes are refused too — the store
|
|
64
|
+
* normalizes `\` to `/` on write, so the stored key would differ from the
|
|
65
|
+
* desired one and the stale pass would delete the file it just wrote.
|
|
66
|
+
*/
|
|
67
|
+
function storeRelPath(relPath: string): string {
|
|
68
|
+
if (
|
|
69
|
+
relPath.includes("\0") ||
|
|
70
|
+
relPath.includes("\\") ||
|
|
71
|
+
posix.isAbsolute(relPath) ||
|
|
72
|
+
relPath.endsWith("/") ||
|
|
73
|
+
relPath.split("/").some((seg) => seg === ".." || seg === ".") ||
|
|
74
|
+
posix.normalize(relPath) !== relPath
|
|
75
|
+
) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Refusing skill file path "${relPath}" (must be a normalized relative path)`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return relPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Last writer wins. A raced conditional put (dual engine pods during a
|
|
85
|
+
* deploy rollover) clears the sink's cached etag, so one retry re-learns
|
|
86
|
+
* the current version and lands.
|
|
87
|
+
*/
|
|
88
|
+
async function putLastWriterWins(
|
|
89
|
+
sink: FileSink,
|
|
90
|
+
key: string,
|
|
91
|
+
body: Uint8Array
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
await sink.put(key, body);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!(error instanceof FileConflictError)) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
await sink.put(key, body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sourceBytes(source: SkillFileSource): Promise<Uint8Array> {
|
|
104
|
+
return "contents" in source
|
|
105
|
+
? Buffer.from(source.contents, "utf8")
|
|
106
|
+
: readFile(source.sourcePath);
|
|
107
|
+
}
|