@cursor/july 0.1.80 → 0.1.83

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 (169) hide show
  1. package/AGENTS.md +0 -12
  2. package/README.md +2 -2
  3. package/dist/channels/github/cursor-account.d.ts +11 -0
  4. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  5. package/dist/channels/github/cursor-account.js +13 -0
  6. package/dist/channels/github/github-channel.d.ts.map +1 -1
  7. package/dist/channels/github/github-channel.js +29 -1
  8. package/dist/channels/github/types.d.ts +8 -0
  9. package/dist/channels/github/types.d.ts.map +1 -1
  10. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  11. package/dist/channels/origin/origin-channel.js +32 -10
  12. package/dist/channels/origin/types.d.ts +7 -0
  13. package/dist/channels/origin/types.d.ts.map +1 -1
  14. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  15. package/dist/channels/slack/cursor-account.js +2 -2
  16. package/dist/channels/slack/dispatch.d.ts +3 -0
  17. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  18. package/dist/channels/slack/dispatch.js +18 -18
  19. package/dist/channels/slack/interactive.d.ts +2 -0
  20. package/dist/channels/slack/interactive.d.ts.map +1 -1
  21. package/dist/channels/slack/interactive.js +4 -16
  22. package/dist/channels/slack/slack-channel.d.ts +1 -1
  23. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  24. package/dist/channels/slack/slack-channel.js +21 -10
  25. package/dist/channels/slack/types.d.ts +7 -0
  26. package/dist/channels/slack/types.d.ts.map +1 -1
  27. package/dist/channels.d.ts.map +1 -1
  28. package/dist/channels.js +4 -0
  29. package/dist/continuation.d.ts +20 -0
  30. package/dist/continuation.d.ts.map +1 -0
  31. package/dist/continuation.js +18 -0
  32. package/dist/docs/404.html +2 -2
  33. package/dist/docs/ab.html +3 -3
  34. package/dist/docs/assets/{app.e4_wtjUS.js → app.6THHGRH-.js} +1 -1
  35. package/dist/docs/assets/chunks/@localSearchIndexroot.D9uScPWb.js +1 -0
  36. package/dist/docs/assets/chunks/{VPLocalSearchBox.DbdzyEie.js → VPLocalSearchBox.BpHB9ulP.js} +1 -1
  37. package/dist/docs/assets/chunks/{theme.D8HpLrLK.js → theme.DBJvJznv.js} +2 -2
  38. package/dist/docs/assets/{guides_cloud-runtime.md.V5igN4Sq.js → guides_cloud-runtime.md.CbklWfzh.js} +1 -1
  39. package/dist/docs/assets/{guides_cloud-runtime.md.V5igN4Sq.lean.js → guides_cloud-runtime.md.CbklWfzh.lean.js} +1 -1
  40. package/dist/docs/assets/{reference_agent-config.md.BZnEKfEc.js → reference_agent-config.md.CTWp4DnU.js} +1 -1
  41. package/dist/docs/assets/{reference_skills.md.B_jHN7JL.js → reference_skills.md.BRF2nDv9.js} +2 -2
  42. package/dist/docs/assets/{reference_skills.md.B_jHN7JL.lean.js → reference_skills.md.BRF2nDv9.lean.js} +1 -1
  43. package/dist/docs/assets/troubleshooting.md.KgmmaCgw.js +1 -0
  44. package/dist/docs/building-with-agents.html +3 -3
  45. package/dist/docs/concepts.html +3 -3
  46. package/dist/docs/deployment.html +3 -3
  47. package/dist/docs/evals.html +3 -3
  48. package/dist/docs/example-agents/approval-buddy.html +3 -3
  49. package/dist/docs/example-agents/benny.html +3 -3
  50. package/dist/docs/example-agents/bugbot.html +3 -3
  51. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  52. package/dist/docs/example-agents/codeowners-review.html +3 -3
  53. package/dist/docs/example-agents/concierge.html +3 -3
  54. package/dist/docs/example-agents/fsd.html +3 -3
  55. package/dist/docs/example-agents/index.html +3 -3
  56. package/dist/docs/example-agents/knowledge-base.html +3 -3
  57. package/dist/docs/example-agents/oncall.html +3 -3
  58. package/dist/docs/example-agents/security-reviewer.html +3 -3
  59. package/dist/docs/example-agents/slack-agent.html +3 -3
  60. package/dist/docs/example-agents/weather-agent.html +3 -3
  61. package/dist/docs/guides/agent-to-agent.html +3 -3
  62. package/dist/docs/guides/cloud-runtime.html +5 -5
  63. package/dist/docs/guides/convert-automation.html +3 -3
  64. package/dist/docs/guides/github.html +3 -3
  65. package/dist/docs/guides/human-in-the-loop.html +3 -3
  66. package/dist/docs/guides/mcp-oauth.html +3 -3
  67. package/dist/docs/guides/opentelemetry.html +3 -3
  68. package/dist/docs/guides/slack.html +3 -3
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +5 -5
  75. package/dist/docs/reference/artifacts.html +3 -3
  76. package/dist/docs/reference/channels.html +3 -3
  77. package/dist/docs/reference/cli.html +3 -3
  78. package/dist/docs/reference/connections.html +3 -3
  79. package/dist/docs/reference/hooks.html +3 -3
  80. package/dist/docs/reference/http-api.html +3 -3
  81. package/dist/docs/reference/instructions.html +3 -3
  82. package/dist/docs/reference/playground.html +3 -3
  83. package/dist/docs/reference/project-layout.html +3 -3
  84. package/dist/docs/reference/prompt.html +3 -3
  85. package/dist/docs/reference/schedules.html +3 -3
  86. package/dist/docs/reference/sessions.html +3 -3
  87. package/dist/docs/reference/skills.html +5 -5
  88. package/dist/docs/reference/subagents.html +3 -3
  89. package/dist/docs/reference/tools.html +3 -3
  90. package/dist/docs/scaffolding-agents.html +3 -3
  91. package/dist/docs/storage.html +3 -3
  92. package/dist/docs/templates/agentic-owners.html +3 -3
  93. package/dist/docs/templates/demo.html +3 -3
  94. package/dist/docs/templates/pr-autofixer.html +3 -3
  95. package/dist/docs/templates/security-reviewer.html +3 -3
  96. package/dist/docs/templates/triage.html +3 -3
  97. package/dist/docs/troubleshooting.html +5 -5
  98. package/dist/files-backends/cursor-hosted.d.ts +6 -0
  99. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  100. package/dist/files-backends/cursor-hosted.js +19 -0
  101. package/dist/files.d.ts +22 -3
  102. package/dist/files.d.ts.map +1 -1
  103. package/dist/files.js +22 -3
  104. package/dist/index.d.ts +5 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +4 -1
  107. package/dist/internal/continuation-channel.d.ts +29 -0
  108. package/dist/internal/continuation-channel.d.ts.map +1 -0
  109. package/dist/internal/continuation-channel.js +88 -0
  110. package/dist/internal/continuation-identity.d.ts +95 -0
  111. package/dist/internal/continuation-identity.d.ts.map +1 -0
  112. package/dist/internal/continuation-identity.js +445 -0
  113. package/dist/internal/deploy-manifest.d.ts +12 -2
  114. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  115. package/dist/internal/deploy-manifest.js +15 -7
  116. package/dist/internal/discovery.d.ts.map +1 -1
  117. package/dist/internal/discovery.js +32 -5
  118. package/dist/internal/sdk-runner.d.ts.map +1 -1
  119. package/dist/internal/sdk-runner.js +6 -9
  120. package/dist/internal/server.d.ts.map +1 -1
  121. package/dist/internal/server.js +18 -8
  122. package/dist/internal/session-engine.d.ts +30 -0
  123. package/dist/internal/session-engine.d.ts.map +1 -1
  124. package/dist/internal/session-engine.js +140 -36
  125. package/dist/internal/session-store.d.ts.map +1 -1
  126. package/dist/internal/session-store.js +6 -0
  127. package/dist/internal/skills-sync.d.ts +27 -0
  128. package/dist/internal/skills-sync.d.ts.map +1 -0
  129. package/dist/internal/skills-sync.js +98 -0
  130. package/dist/playground/assets/{index-DJzcIeGt.css → index-ChETdG-_.css} +1 -1
  131. package/dist/playground/assets/{index-K3Sxyx0I.js → index-Cspgk9jC.js} +45 -45
  132. package/dist/playground/index.html +2 -2
  133. package/dist/types.d.ts +49 -0
  134. package/dist/types.d.ts.map +1 -1
  135. package/docs/guides/cloud-runtime.md +6 -1
  136. package/docs/reference/agent-config.md +6 -5
  137. package/docs/reference/skills.md +12 -3
  138. package/docs/troubleshooting.md +1 -1
  139. package/package.json +1 -1
  140. package/skills/framework-map/SKILL.md +2 -1
  141. package/src/channels/github/cursor-account.ts +20 -0
  142. package/src/channels/github/github-channel.ts +52 -1
  143. package/src/channels/github/types.ts +8 -0
  144. package/src/channels/origin/origin-channel.ts +48 -10
  145. package/src/channels/origin/types.ts +7 -0
  146. package/src/channels/slack/cursor-account.ts +1 -0
  147. package/src/channels/slack/dispatch.ts +44 -2
  148. package/src/channels/slack/interactive.ts +5 -0
  149. package/src/channels/slack/slack-channel.ts +23 -2
  150. package/src/channels/slack/types.ts +7 -0
  151. package/src/channels.ts +7 -0
  152. package/src/continuation.ts +49 -0
  153. package/src/files-backends/cursor-hosted.ts +21 -0
  154. package/src/files.ts +29 -3
  155. package/src/index.ts +30 -1
  156. package/src/internal/continuation-channel.ts +129 -0
  157. package/src/internal/continuation-identity.ts +597 -0
  158. package/src/internal/deploy-manifest.ts +36 -3
  159. package/src/internal/discovery.ts +40 -2
  160. package/src/internal/sdk-runner.ts +10 -11
  161. package/src/internal/server.ts +23 -9
  162. package/src/internal/session-engine.ts +178 -37
  163. package/src/internal/session-store.ts +6 -0
  164. package/src/internal/skills-sync.ts +110 -0
  165. package/src/types.ts +60 -0
  166. package/dist/docs/assets/chunks/@localSearchIndexroot.BGiMJpnL.js +0 -1
  167. package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +0 -1
  168. /package/dist/docs/assets/{reference_agent-config.md.BZnEKfEc.lean.js → reference_agent-config.md.CTWp4DnU.lean.js} +0 -0
  169. /package/dist/docs/assets/{troubleshooting.md.DQ-hYuZT.lean.js → troubleshooting.md.KgmmaCgw.lean.js} +0 -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 through native store discovery: hosted deployments write the deployment store's skills/ directory; local serve/run with a personal API key writes agent-serve/<agent>/skills/ on the USER store. Without a reachable store, only skills already in the cloud repo are visible.",
1553
1591
  });
1554
1592
  }
1555
1593
  if (counts.seedFiles > 0) {
@@ -514,7 +514,7 @@ export class CursorSdkRunner implements AgentRunner {
514
514
  request: RunnerTurnRequest,
515
515
  shared: Pick<AgentOptions, "model" | "apiKey" | "mcpServers" | "agents">
516
516
  ): Promise<SDKAgent> {
517
- const cloud = toCloudAgentOptions(request.cloud);
517
+ const cloud = toCloudAgentOptions(request.cloud, request.agentServeAgent);
518
518
  const hasEnvVars =
519
519
  cloud.envVars !== undefined && Object.keys(cloud.envVars).length > 0;
520
520
  const options: AgentOptions = {
@@ -603,24 +603,23 @@ export function toModelSelection(model: ModelSetting): ModelSelection {
603
603
  }
604
604
 
605
605
  function toCloudAgentOptions(
606
- cloud: RunnerTurnRequest["cloud"]
606
+ cloud: RunnerTurnRequest["cloud"],
607
+ agentServeAgent?: string
607
608
  ): CloudAgentOptions {
608
- if (cloud === undefined) {
609
- return {};
610
- }
611
609
  return {
612
- ...(cloud.env === undefined ? {} : { env: cloud.env }),
613
- ...(cloud.repos === undefined ? {} : { repos: cloud.repos }),
614
- ...(cloud.workOnCurrentBranch === undefined
610
+ ...(cloud?.env === undefined ? {} : { env: cloud.env }),
611
+ ...(cloud?.repos === undefined ? {} : { repos: cloud.repos }),
612
+ ...(cloud?.workOnCurrentBranch === undefined
615
613
  ? {}
616
614
  : { workOnCurrentBranch: cloud.workOnCurrentBranch }),
617
- ...(cloud.autoCreatePR === undefined
615
+ ...(cloud?.autoCreatePR === undefined
618
616
  ? {}
619
617
  : { autoCreatePR: cloud.autoCreatePR }),
620
- ...(cloud.skipReviewerRequest === undefined
618
+ ...(cloud?.skipReviewerRequest === undefined
621
619
  ? {}
622
620
  : { skipReviewerRequest: cloud.skipReviewerRequest }),
623
- ...(cloud.envVars === undefined ? {} : { envVars: cloud.envVars }),
621
+ ...(cloud?.envVars === undefined ? {} : { envVars: cloud.envVars }),
622
+ ...(agentServeAgent === undefined ? {} : { agentServeAgent }),
624
623
  };
625
624
  }
626
625
 
@@ -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,15 @@ 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
+ AGENT_SERVE_USER_STORE_FILES_SOURCE_ID,
29
+ agentStoreKeys,
30
+ } from "../files.js";
31
+ import {
32
+ cursorHostedFiles,
33
+ isCursorHostedFilesAvailable,
34
+ isCursorUserStoreFilesAvailable,
35
+ } from "../files-backends/cursor-hosted.js";
28
36
  import {
29
37
  type AgentProject,
30
38
  type ApprovalDecision,
@@ -80,6 +88,16 @@ import {
80
88
  isUnpricedCompletedTurn,
81
89
  mapSdkTokenUsage,
82
90
  } from "./cloud-turn-cost.js";
91
+ import {
92
+ applyAgentContinuationDefaults,
93
+ channelContinuationIdentity,
94
+ httpContinuationIdentity,
95
+ } from "./continuation-channel.js";
96
+ import {
97
+ continuationSource,
98
+ resolveV2Key,
99
+ V2EngineError,
100
+ } from "./continuation-identity.js";
83
101
  import { isDevMode } from "./dev-mode.js";
84
102
  import { type EventLogMemoryStats, EventLogStore } from "./event-log.js";
85
103
  import { TurnEventMapper } from "./event-mapper.js";
@@ -113,6 +131,7 @@ import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections
113
131
  import type { AgentRunner } from "./sdk-runner.js";
114
132
  import { sessionExternalLinks } from "./session-links.js";
115
133
  import { SessionStore } from "./session-store.js";
134
+ import { syncSkillsToStore } from "./skills-sync.js";
116
135
  import { StorageCoordinator } from "./storage-coordinator.js";
117
136
  import type { FrameworkSessionStore } from "./storage-roles.js";
118
137
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
@@ -302,6 +321,8 @@ export class SessionEngine {
302
321
  >();
303
322
  /** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
304
323
  private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
324
+ /** In-flight skills → Agent Store sync (see ensureCloudSkillsSynced). */
325
+ private cloudSkillsSync: Promise<boolean> | undefined;
305
326
 
306
327
  private readonly runner: AgentRunner;
307
328
  private readonly logger: (line: string) => void;
@@ -366,6 +387,7 @@ export class SessionEngine {
366
387
 
367
388
  constructor(options: SessionEngineOptions) {
368
389
  this.project = options.project;
390
+ applyAgentContinuationDefaults(this.project);
369
391
  this.stateRoot = options.stateRoot;
370
392
  this.runner = options.runner;
371
393
  this.logger = options.logger ?? (() => {});
@@ -734,26 +756,50 @@ export class SessionEngine {
734
756
  * Start or resume a session on a channel. When `continuationToken`
735
757
  * addresses an existing session this is a follow-up turn; otherwise a new
736
758
  * session is created (keyed by the provided token, or a fresh one).
759
+ *
760
+ * v2 (`/v2` selectors) refuses `continuationToken` and looks
761
+ * up `(agentId, cKey)` from a mailbox stamp or structured claim.
737
762
  */
738
763
  async send(
739
764
  channelId: string,
740
765
  message: string,
741
766
  options: EngineSendOptions = {}
742
767
  ): Promise<ChannelSession> {
743
- const auth = options.auth ?? null;
768
+ const identity =
769
+ channelId === HTTP_CHANNEL_ID
770
+ ? httpContinuationIdentity(this.project.httpChannel)
771
+ : channelContinuationIdentity(this.channelsById.get(channelId));
772
+ let sendOptions = options;
773
+ let skipContinuationLookup = false;
774
+ if (continuationSource(identity) === "control-plane") {
775
+ if (options.continuationToken !== undefined) {
776
+ throw new V2EngineError(
777
+ "v2: do not pass continuationToken; use mailboxContinuation or claim"
778
+ );
779
+ }
780
+ const cKey = resolveV2Key(identity, options);
781
+ sendOptions = { ...options, continuationToken: cKey };
782
+ skipContinuationLookup = cKey.startsWith("evt:");
783
+ }
784
+
785
+ const auth = sendOptions.auth ?? null;
744
786
  let existing =
745
- options.continuationToken === undefined
787
+ skipContinuationLookup || sendOptions.continuationToken === undefined
746
788
  ? undefined
747
789
  : await this.sessions.findByContinuation(
748
790
  channelId,
749
- options.continuationToken
791
+ sendOptions.continuationToken
750
792
  );
751
- if (existing === undefined && options.continuationToken !== undefined) {
793
+ if (
794
+ existing === undefined &&
795
+ !skipContinuationLookup &&
796
+ sendOptions.continuationToken !== undefined
797
+ ) {
752
798
  // Local miss: the session may live only in the storage sink (it
753
799
  // fell outside the startup restore window, or restore is lazy-only).
754
800
  existing = await this.restoreSessionByContinuation(
755
801
  channelId,
756
- options.continuationToken
802
+ sendOptions.continuationToken
757
803
  );
758
804
  }
759
805
 
@@ -765,25 +811,25 @@ export class SessionEngine {
765
811
  // synchronize). Leave the durable continuation key unchanged.
766
812
  let record = existing;
767
813
  const refreshWorkspace =
768
- options.workspaceDir !== undefined &&
769
- options.workspaceDir !== existing.workspaceOverride;
814
+ sendOptions.workspaceDir !== undefined &&
815
+ sendOptions.workspaceDir !== existing.workspaceOverride;
770
816
  if (
771
817
  refreshWorkspace ||
772
- options.workspaceFiles !== undefined ||
773
- options.images !== undefined
818
+ sendOptions.workspaceFiles !== undefined ||
819
+ sendOptions.images !== undefined
774
820
  ) {
775
821
  record = await this.updateRecord(existing.sessionId, (current) => ({
776
822
  ...current,
777
823
  workspaceOverride: refreshWorkspace
778
- ? options.workspaceDir
824
+ ? sendOptions.workspaceDir
779
825
  : current.workspaceOverride,
780
826
  pendingWorkspaceFiles: mergePendingWorkspaceFiles(
781
827
  current.pendingWorkspaceFiles,
782
- options.workspaceFiles
828
+ sendOptions.workspaceFiles
783
829
  ),
784
830
  pendingImages: mergePendingImages(
785
831
  current.pendingImages,
786
- options.images
832
+ sendOptions.images
787
833
  ),
788
834
  }));
789
835
  }
@@ -792,31 +838,32 @@ export class SessionEngine {
792
838
  // and the first send's snapshot must not pin this session's turns to a
793
839
  // stale repo/prUrl for the session's lifetime.
794
840
  if (
795
- options.cloud !== undefined &&
796
- JSON.stringify(options.cloud) !== JSON.stringify(record.cloudOverride)
841
+ sendOptions.cloud !== undefined &&
842
+ JSON.stringify(sendOptions.cloud) !==
843
+ JSON.stringify(record.cloudOverride)
797
844
  ) {
798
845
  record = await this.updateRecord(record.sessionId, (current) => ({
799
846
  ...current,
800
- cloudOverride: options.cloud,
847
+ cloudOverride: sendOptions.cloud,
801
848
  }));
802
849
  }
803
850
  // Refresh wake-derived channel state (GitHub headSha / refs) without
804
851
  // wiping sticky fields. Null patch values are skipped.
805
- if (options.refreshState !== undefined) {
852
+ if (sendOptions.refreshState !== undefined) {
806
853
  record = await this.updateRecord(record.sessionId, (current) => ({
807
854
  ...current,
808
855
  channelState: mergeChannelStateSkipNull(
809
856
  current.channelState,
810
- options.refreshState
857
+ sendOptions.refreshState
811
858
  ),
812
859
  }));
813
860
  }
814
861
  const since = (await this.logs.get(record.sessionId)).count;
815
862
  const coalesced = await this.startTurn(record.sessionId, message, {
816
- admission: options.admission ?? "preempt",
817
- ...(options.coalesceSourceTs === undefined
863
+ admission: sendOptions.admission ?? "preempt",
864
+ ...(sendOptions.coalesceSourceTs === undefined
818
865
  ? {}
819
- : { sourceTs: options.coalesceSourceTs }),
866
+ : { sourceTs: sendOptions.coalesceSourceTs }),
820
867
  });
821
868
  return this.channelSession(
822
869
  record.sessionId,
@@ -828,22 +875,22 @@ export class SessionEngine {
828
875
  }
829
876
 
830
877
  const now = new Date().toISOString();
831
- const channelState = this.initialChannelState(channelId, options.state);
832
- const purpose = options.purpose === "eval" ? "eval" : "live";
878
+ const channelState = this.initialChannelState(channelId, sendOptions.state);
879
+ const purpose = sendOptions.purpose === "eval" ? "eval" : "live";
833
880
  let record: SessionRecord = {
834
881
  sessionId: newSessionId(),
835
882
  channelId,
836
- mode: options.mode ?? "chat",
883
+ mode: sendOptions.mode ?? "chat",
837
884
  purpose,
838
- continuationKey: options.continuationToken ?? newContinuationKey(),
885
+ continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
839
886
  auth,
840
- title: options.title,
887
+ title: sendOptions.title,
841
888
  channelState,
842
- workspaceOverride: options.workspaceDir,
843
- pendingWorkspaceFiles: options.workspaceFiles,
844
- pendingImages: options.images,
845
- cloudOverride: options.cloud,
846
- sdkAgentId: options.sdkAgentId,
889
+ workspaceOverride: sendOptions.workspaceDir,
890
+ pendingWorkspaceFiles: sendOptions.workspaceFiles,
891
+ pendingImages: sendOptions.images,
892
+ cloudOverride: sendOptions.cloud,
893
+ sdkAgentId: sendOptions.sdkAgentId,
847
894
  turnCount: 0,
848
895
  createdAt: now,
849
896
  updatedAt: now,
@@ -857,10 +904,10 @@ export class SessionEngine {
857
904
  record = await this.enrollABs(record);
858
905
  const since = (await this.logs.get(record.sessionId)).count;
859
906
  await this.startTurn(record.sessionId, message, {
860
- admission: options.admission ?? "preempt",
861
- ...(options.coalesceSourceTs === undefined
907
+ admission: sendOptions.admission ?? "preempt",
908
+ ...(sendOptions.coalesceSourceTs === undefined
862
909
  ? {}
863
- : { sourceTs: options.coalesceSourceTs }),
910
+ : { sourceTs: sendOptions.coalesceSourceTs }),
864
911
  });
865
912
  return this.channelSession(record.sessionId, record, true, false, since);
866
913
  }
@@ -1753,6 +1800,88 @@ export class SessionEngine {
1753
1800
  };
1754
1801
  }
1755
1802
 
1803
+ /**
1804
+ * Sync authored skills onto the Agent Store at `skills/<name>/…` (hosted)
1805
+ * or `agent-serve/<agent>/skills/<name>/…` (personal-key local+cloud)
1806
+ * before the first cloud prompt, so `--agent-store-skills-dir` and the
1807
+ * first-turn S3 scan can discover them. False when the sync fails: the
1808
+ * turn then runs without store skills rather than pointing discovery at
1809
+ * paths that do not exist.
1810
+ *
1811
+ * With skills authored, every first cloud turn re-copies the authored
1812
+ * bytes and the memo only dedupes concurrent first turns. The mount is
1813
+ * writable from cloud sessions, so a once-per-process copy would let one
1814
+ * session's edits masquerade as agent definition for every later session
1815
+ * until the engine restarts; re-syncing keeps the store true to discovery
1816
+ * the same way local materialization always rewrites skills. Without
1817
+ * skills the sync is best-effort stale cleanup and one attempt per
1818
+ * process is enough, success or not — a deployment without the files
1819
+ * surface must not fail and log on every new session for a feature it
1820
+ * does not use.
1821
+ */
1822
+ private ensureCloudSkillsSynced(): Promise<boolean> {
1823
+ if (this.cloudSkillsSync !== undefined) {
1824
+ return this.cloudSkillsSync;
1825
+ }
1826
+ const target = this.cloudSkillsStore();
1827
+ if (target === undefined) {
1828
+ this.cloudSkillsSync = Promise.resolve(false);
1829
+ return this.cloudSkillsSync;
1830
+ }
1831
+ const skills = this.project.agent.skills;
1832
+ let sync = syncSkillsToStore({
1833
+ sink: target.sink,
1834
+ agentName: this.project.name,
1835
+ skills,
1836
+ skillsPrefix: target.skillsPrefix,
1837
+ }).then(
1838
+ () => true,
1839
+ (error) => {
1840
+ this.logger(
1841
+ `[skills] ${target.label} sync failed: ${error instanceof Error ? error.message : String(error)}`
1842
+ );
1843
+ return false;
1844
+ }
1845
+ );
1846
+ if (skills.length > 0) {
1847
+ sync = sync.finally(() => {
1848
+ this.cloudSkillsSync = undefined;
1849
+ });
1850
+ }
1851
+ this.cloudSkillsSync = sync;
1852
+ return sync;
1853
+ }
1854
+
1855
+ /**
1856
+ * Hosted deployments own the deployment store. Local `serve`/`run` with
1857
+ * a personal API key writes into the USER store under `agent-serve/`.
1858
+ */
1859
+ private cloudSkillsStore():
1860
+ | {
1861
+ sink: ReturnType<typeof cursorHostedFiles>;
1862
+ skillsPrefix: string;
1863
+ label: string;
1864
+ }
1865
+ | undefined {
1866
+ if (isCursorHostedFilesAvailable()) {
1867
+ return {
1868
+ sink: cursorHostedFiles(),
1869
+ skillsPrefix: agentStoreKeys.skillsPrefix(),
1870
+ label: "deployment store",
1871
+ };
1872
+ }
1873
+ if (isCursorUserStoreFilesAvailable()) {
1874
+ return {
1875
+ sink: cursorHostedFiles({
1876
+ storeSourceId: AGENT_SERVE_USER_STORE_FILES_SOURCE_ID,
1877
+ }),
1878
+ skillsPrefix: agentStoreKeys.userSkillsPrefix(this.project.name),
1879
+ label: "user store",
1880
+ };
1881
+ }
1882
+ return undefined;
1883
+ }
1884
+
1756
1885
  private async buildTurnRequest(
1757
1886
  record: SessionRecord,
1758
1887
  agent: ResolvedAgent,
@@ -1815,8 +1944,9 @@ export class SessionEngine {
1815
1944
  if (isFirstTurn) {
1816
1945
  if (runtime === "cloud") {
1817
1946
  // 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
1947
+ // including agent tool script bodies. On hosted deployments authored
1948
+ // skills land on the store mount for native discovery, and the
1949
+ // shared memory journal is on that mount too — say where. When the
1820
1950
  // turn carries the session-bound server-tool MCP endpoint, the
1821
1951
  // preamble catalogs server tools under that MCP server name *before*
1822
1952
  // the agent-tool "write the script if missing" recipe, so a model
@@ -1828,7 +1958,15 @@ export class SessionEngine {
1828
1958
  ? undefined
1829
1959
  : SERVER_TOOLS_MCP_SERVER_NAME,
1830
1960
  });
1831
- const memorySection = isCursorHostedFilesAvailable()
1961
+ const hosted = isCursorHostedFilesAvailable();
1962
+ // Synced whenever a store is reachable, not just when skills exist:
1963
+ // removing the last skill has to delete its store copies too.
1964
+ // Discovery is native (`--agent-store-skills-dir`); this only has
1965
+ // to land the bytes.
1966
+ if (hosted || isCursorUserStoreFilesAvailable()) {
1967
+ await this.ensureCloudSkillsSynced();
1968
+ }
1969
+ const memorySection = hosted
1832
1970
  ? `\n\n${buildCloudMemorySection(this.project.name)}`
1833
1971
  : "";
1834
1972
  turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
@@ -1859,6 +1997,9 @@ export class SessionEngine {
1859
1997
  ...(agent.model === undefined ? {} : { model: agent.model }),
1860
1998
  runtime,
1861
1999
  ...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
2000
+ ...(runtime === "cloud" && !isCursorHostedFilesAvailable()
2001
+ ? { agentServeAgent: this.project.name }
2002
+ : {}),
1862
2003
  customTools:
1863
2004
  runtime === "local"
1864
2005
  ? await this.buildCustomTools(record, agent, turnId, emit)
@@ -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