@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.
Files changed (145) hide show
  1. package/AGENTS.md +0 -12
  2. package/dist/channels/github/cursor-account.d.ts +11 -0
  3. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  4. package/dist/channels/github/cursor-account.js +13 -0
  5. package/dist/channels/github/github-channel.d.ts.map +1 -1
  6. package/dist/channels/github/github-channel.js +29 -1
  7. package/dist/channels/github/types.d.ts +8 -0
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  10. package/dist/channels/origin/origin-channel.js +32 -10
  11. package/dist/channels/origin/types.d.ts +7 -0
  12. package/dist/channels/origin/types.d.ts.map +1 -1
  13. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  14. package/dist/channels/slack/cursor-account.js +2 -2
  15. package/dist/channels/slack/dispatch.d.ts +3 -0
  16. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  17. package/dist/channels/slack/dispatch.js +18 -18
  18. package/dist/channels/slack/interactive.d.ts +2 -0
  19. package/dist/channels/slack/interactive.d.ts.map +1 -1
  20. package/dist/channels/slack/interactive.js +4 -16
  21. package/dist/channels/slack/slack-channel.d.ts +1 -1
  22. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  23. package/dist/channels/slack/slack-channel.js +21 -10
  24. package/dist/channels/slack/types.d.ts +7 -0
  25. package/dist/channels/slack/types.d.ts.map +1 -1
  26. package/dist/channels.d.ts.map +1 -1
  27. package/dist/channels.js +4 -0
  28. package/dist/continuation.d.ts +20 -0
  29. package/dist/continuation.d.ts.map +1 -0
  30. package/dist/continuation.js +18 -0
  31. package/dist/docs/404.html +1 -1
  32. package/dist/docs/ab.html +2 -2
  33. package/dist/docs/assets/{app.e4_wtjUS.js → app.BEHVaPKt.js} +1 -1
  34. package/dist/docs/assets/chunks/{@localSearchIndexroot.BGiMJpnL.js → @localSearchIndexroot.Vvio4stV.js} +1 -1
  35. package/dist/docs/assets/chunks/{VPLocalSearchBox.DbdzyEie.js → VPLocalSearchBox.D2-5PKih.js} +1 -1
  36. package/dist/docs/assets/chunks/{theme.D8HpLrLK.js → theme.uNqJ4MUO.js} +2 -2
  37. package/dist/docs/building-with-agents.html +2 -2
  38. package/dist/docs/concepts.html +2 -2
  39. package/dist/docs/deployment.html +2 -2
  40. package/dist/docs/evals.html +2 -2
  41. package/dist/docs/example-agents/approval-buddy.html +2 -2
  42. package/dist/docs/example-agents/benny.html +2 -2
  43. package/dist/docs/example-agents/bugbot.html +2 -2
  44. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  45. package/dist/docs/example-agents/codeowners-review.html +2 -2
  46. package/dist/docs/example-agents/concierge.html +2 -2
  47. package/dist/docs/example-agents/fsd.html +2 -2
  48. package/dist/docs/example-agents/index.html +2 -2
  49. package/dist/docs/example-agents/knowledge-base.html +2 -2
  50. package/dist/docs/example-agents/oncall.html +2 -2
  51. package/dist/docs/example-agents/security-reviewer.html +2 -2
  52. package/dist/docs/example-agents/slack-agent.html +2 -2
  53. package/dist/docs/example-agents/weather-agent.html +2 -2
  54. package/dist/docs/guides/agent-to-agent.html +2 -2
  55. package/dist/docs/guides/cloud-runtime.html +2 -2
  56. package/dist/docs/guides/convert-automation.html +2 -2
  57. package/dist/docs/guides/github.html +2 -2
  58. package/dist/docs/guides/human-in-the-loop.html +2 -2
  59. package/dist/docs/guides/mcp-oauth.html +2 -2
  60. package/dist/docs/guides/opentelemetry.html +2 -2
  61. package/dist/docs/guides/slack.html +2 -2
  62. package/dist/docs/guides/webhooks.html +2 -2
  63. package/dist/docs/hillclimbing.html +2 -2
  64. package/dist/docs/index.html +2 -2
  65. package/dist/docs/quickstart.html +2 -2
  66. package/dist/docs/reference/agent-config.html +2 -2
  67. package/dist/docs/reference/artifacts.html +2 -2
  68. package/dist/docs/reference/channels.html +2 -2
  69. package/dist/docs/reference/cli.html +2 -2
  70. package/dist/docs/reference/connections.html +2 -2
  71. package/dist/docs/reference/hooks.html +2 -2
  72. package/dist/docs/reference/http-api.html +2 -2
  73. package/dist/docs/reference/instructions.html +2 -2
  74. package/dist/docs/reference/playground.html +2 -2
  75. package/dist/docs/reference/project-layout.html +2 -2
  76. package/dist/docs/reference/prompt.html +2 -2
  77. package/dist/docs/reference/schedules.html +2 -2
  78. package/dist/docs/reference/sessions.html +2 -2
  79. package/dist/docs/reference/skills.html +2 -2
  80. package/dist/docs/reference/subagents.html +2 -2
  81. package/dist/docs/reference/tools.html +2 -2
  82. package/dist/docs/scaffolding-agents.html +2 -2
  83. package/dist/docs/storage.html +2 -2
  84. package/dist/docs/templates/agentic-owners.html +2 -2
  85. package/dist/docs/templates/demo.html +2 -2
  86. package/dist/docs/templates/pr-autofixer.html +2 -2
  87. package/dist/docs/templates/security-reviewer.html +2 -2
  88. package/dist/docs/templates/triage.html +2 -2
  89. package/dist/docs/troubleshooting.html +2 -2
  90. package/dist/files.d.ts +9 -3
  91. package/dist/files.d.ts.map +1 -1
  92. package/dist/files.js +9 -3
  93. package/dist/index.d.ts +5 -1
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +4 -1
  96. package/dist/internal/continuation-channel.d.ts +29 -0
  97. package/dist/internal/continuation-channel.d.ts.map +1 -0
  98. package/dist/internal/continuation-channel.js +88 -0
  99. package/dist/internal/continuation-identity.d.ts +95 -0
  100. package/dist/internal/continuation-identity.d.ts.map +1 -0
  101. package/dist/internal/continuation-identity.js +445 -0
  102. package/dist/internal/deploy-manifest.d.ts +12 -2
  103. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  104. package/dist/internal/deploy-manifest.js +15 -7
  105. package/dist/internal/discovery.d.ts.map +1 -1
  106. package/dist/internal/discovery.js +32 -5
  107. package/dist/internal/server.d.ts.map +1 -1
  108. package/dist/internal/server.js +18 -8
  109. package/dist/internal/session-engine.d.ts +24 -0
  110. package/dist/internal/session-engine.d.ts.map +1 -1
  111. package/dist/internal/session-engine.js +104 -34
  112. package/dist/internal/session-store.d.ts.map +1 -1
  113. package/dist/internal/session-store.js +6 -0
  114. package/dist/internal/skills-sync.d.ts +25 -0
  115. package/dist/internal/skills-sync.d.ts.map +1 -0
  116. package/dist/internal/skills-sync.js +96 -0
  117. package/dist/playground/assets/{index-DJzcIeGt.css → index-ChETdG-_.css} +1 -1
  118. package/dist/playground/assets/{index-K3Sxyx0I.js → index-CkuIrhg4.js} +3 -3
  119. package/dist/playground/index.html +2 -2
  120. package/dist/types.d.ts +44 -0
  121. package/dist/types.d.ts.map +1 -1
  122. package/package.json +1 -1
  123. package/src/channels/github/cursor-account.ts +20 -0
  124. package/src/channels/github/github-channel.ts +52 -1
  125. package/src/channels/github/types.ts +8 -0
  126. package/src/channels/origin/origin-channel.ts +48 -10
  127. package/src/channels/origin/types.ts +7 -0
  128. package/src/channels/slack/cursor-account.ts +1 -0
  129. package/src/channels/slack/dispatch.ts +44 -2
  130. package/src/channels/slack/interactive.ts +5 -0
  131. package/src/channels/slack/slack-channel.ts +23 -2
  132. package/src/channels/slack/types.ts +7 -0
  133. package/src/channels.ts +7 -0
  134. package/src/continuation.ts +49 -0
  135. package/src/files.ts +10 -3
  136. package/src/index.ts +30 -1
  137. package/src/internal/continuation-channel.ts +129 -0
  138. package/src/internal/continuation-identity.ts +597 -0
  139. package/src/internal/deploy-manifest.ts +36 -3
  140. package/src/internal/discovery.ts +40 -2
  141. package/src/internal/server.ts +23 -9
  142. package/src/internal/session-engine.ts +132 -37
  143. package/src/internal/session-store.ts +6 -0
  144. package/src/internal/skills-sync.ts +107 -0
  145. 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: httpChannelDefinition ?? 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 are materialized into the local session workspace only; cloud agents see skills that already exist in the cloud repo.",
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) {
@@ -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
- resolvedCursorGithubCredentials === undefined
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 { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
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 auth = options.auth ?? null;
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
- options.continuationToken === undefined
782
+ skipContinuationLookup || sendOptions.continuationToken === undefined
746
783
  ? undefined
747
784
  : await this.sessions.findByContinuation(
748
785
  channelId,
749
- options.continuationToken
786
+ sendOptions.continuationToken
750
787
  );
751
- if (existing === undefined && options.continuationToken !== undefined) {
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
- options.continuationToken
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
- options.workspaceDir !== undefined &&
769
- options.workspaceDir !== existing.workspaceOverride;
809
+ sendOptions.workspaceDir !== undefined &&
810
+ sendOptions.workspaceDir !== existing.workspaceOverride;
770
811
  if (
771
812
  refreshWorkspace ||
772
- options.workspaceFiles !== undefined ||
773
- options.images !== undefined
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
- ? options.workspaceDir
819
+ ? sendOptions.workspaceDir
779
820
  : current.workspaceOverride,
780
821
  pendingWorkspaceFiles: mergePendingWorkspaceFiles(
781
822
  current.pendingWorkspaceFiles,
782
- options.workspaceFiles
823
+ sendOptions.workspaceFiles
783
824
  ),
784
825
  pendingImages: mergePendingImages(
785
826
  current.pendingImages,
786
- options.images
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
- options.cloud !== undefined &&
796
- JSON.stringify(options.cloud) !== JSON.stringify(record.cloudOverride)
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: options.cloud,
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 (options.refreshState !== undefined) {
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
- options.refreshState
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: options.admission ?? "preempt",
817
- ...(options.coalesceSourceTs === undefined
858
+ admission: sendOptions.admission ?? "preempt",
859
+ ...(sendOptions.coalesceSourceTs === undefined
818
860
  ? {}
819
- : { sourceTs: options.coalesceSourceTs }),
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, options.state);
832
- const purpose = options.purpose === "eval" ? "eval" : "live";
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: options.mode ?? "chat",
878
+ mode: sendOptions.mode ?? "chat",
837
879
  purpose,
838
- continuationKey: options.continuationToken ?? newContinuationKey(),
880
+ continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
839
881
  auth,
840
- title: options.title,
882
+ title: sendOptions.title,
841
883
  channelState,
842
- workspaceOverride: options.workspaceDir,
843
- pendingWorkspaceFiles: options.workspaceFiles,
844
- pendingImages: options.images,
845
- cloudOverride: options.cloud,
846
- sdkAgentId: options.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: options.admission ?? "preempt",
861
- ...(options.coalesceSourceTs === undefined
902
+ admission: sendOptions.admission ?? "preempt",
903
+ ...(sendOptions.coalesceSourceTs === undefined
862
904
  ? {}
863
- : { sourceTs: options.coalesceSourceTs }),
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 the
1819
- // shared memory journal is on the store mount say where. When the
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 memorySection = isCursorHostedFilesAvailable()
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
+ }