@mastra/factory 0.8.0-alpha.14 → 0.8.0-alpha.16

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 (73) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +30 -2
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/base.d.ts +8 -0
  6. package/dist/integrations/base.d.ts.map +1 -1
  7. package/dist/integrations/github/integration.d.ts.map +1 -1
  8. package/dist/integrations/github/integration.js +3 -1
  9. package/dist/integrations/github/integration.js.map +1 -1
  10. package/dist/integrations/github/reconcile-worker.d.ts +5 -0
  11. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  12. package/dist/integrations/github/reconcile-worker.js +7 -0
  13. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  14. package/dist/integrations/github/routes.d.ts.map +1 -1
  15. package/dist/integrations/github/routes.js +7 -3
  16. package/dist/integrations/github/routes.js.map +1 -1
  17. package/dist/integrations/github/sandbox.d.ts +31 -1
  18. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  19. package/dist/integrations/github/sandbox.js +12 -6
  20. package/dist/integrations/github/sandbox.js.map +1 -1
  21. package/dist/integrations/platform/github/integration.d.ts +7 -0
  22. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  23. package/dist/integrations/platform/github/integration.js +32 -3
  24. package/dist/integrations/platform/github/integration.js.map +1 -1
  25. package/dist/routes/config.d.ts +8 -6
  26. package/dist/routes/config.d.ts.map +1 -1
  27. package/dist/routes/config.js +91 -56
  28. package/dist/routes/config.js.map +1 -1
  29. package/dist/routes/projects.d.ts +9 -1
  30. package/dist/routes/projects.d.ts.map +1 -1
  31. package/dist/routes/projects.js +8 -0
  32. package/dist/routes/projects.js.map +1 -1
  33. package/dist/routes/surface.d.ts +5 -0
  34. package/dist/routes/surface.d.ts.map +1 -1
  35. package/dist/routes/surface.js +2 -0
  36. package/dist/routes/surface.js.map +1 -1
  37. package/dist/sandbox/base-checkpoint-triggers.d.ts +77 -0
  38. package/dist/sandbox/base-checkpoint-triggers.d.ts.map +1 -0
  39. package/dist/sandbox/base-checkpoint-triggers.js +137 -0
  40. package/dist/sandbox/base-checkpoint-triggers.js.map +1 -0
  41. package/dist/sandbox/base-checkpoint.d.ts +59 -0
  42. package/dist/sandbox/base-checkpoint.d.ts.map +1 -0
  43. package/dist/sandbox/base-checkpoint.js +145 -0
  44. package/dist/sandbox/base-checkpoint.js.map +1 -0
  45. package/dist/sandbox/fleet.d.ts +32 -0
  46. package/dist/sandbox/fleet.d.ts.map +1 -1
  47. package/dist/sandbox/fleet.js +39 -10
  48. package/dist/sandbox/fleet.js.map +1 -1
  49. package/dist/session/checkpoint-capture.d.ts +3 -2
  50. package/dist/session/checkpoint-capture.d.ts.map +1 -1
  51. package/dist/session/checkpoint-capture.js +3 -3
  52. package/dist/session/checkpoint-capture.js.map +1 -1
  53. package/dist/session/model-pack-hydration.d.ts +55 -0
  54. package/dist/session/model-pack-hydration.d.ts.map +1 -0
  55. package/dist/session/model-pack-hydration.js +56 -0
  56. package/dist/session/model-pack-hydration.js.map +1 -0
  57. package/dist/storage/domains/model-packs/base.d.ts +26 -0
  58. package/dist/storage/domains/model-packs/base.d.ts.map +1 -1
  59. package/dist/storage/domains/model-packs/base.js +102 -13
  60. package/dist/storage/domains/model-packs/base.js.map +1 -1
  61. package/dist/storage/domains/source-control/base.d.ts +28 -0
  62. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  63. package/dist/storage/domains/source-control/base.js +70 -14
  64. package/dist/storage/domains/source-control/base.js.map +1 -1
  65. package/dist/storage/domains/source-control/inmemory.d.ts +9 -1
  66. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  67. package/dist/storage/domains/source-control/inmemory.js +8 -0
  68. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  69. package/dist/workspace.d.ts +31 -2
  70. package/dist/workspace.d.ts.map +1 -1
  71. package/dist/workspace.js +224 -53
  72. package/dist/workspace.js.map +1 -1
  73. package/package.json +6 -6
@@ -1,3 +1,4 @@
1
+ import { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';
1
2
  import { getDynamicWorkspace } from '@mastra/code-sdk/agents/workspace';
2
3
  import { LocalSandbox, Workspace } from '@mastra/core/workspace';
3
4
  import type { MastraFactorySandboxConfig } from './factory.js';
@@ -5,6 +6,34 @@ import type { GithubIntegration } from './integrations/github/integration.js';
5
6
  import type { SandboxFleet } from './sandbox/fleet.js';
6
7
  import type { WorkItemsStorage } from './storage/domains/work-items/base.js';
7
8
  export declare function checkpointNameForSession(sessionId: string): string;
9
+ /**
10
+ * Whether a command failure means the sandbox itself is gone (destroyed by
11
+ * idle GC or provider teardown) AND the command provably never started, so
12
+ * reviving the sandbox and replaying the command cannot run a side effect
13
+ * twice. Matched by error name so any provider's equivalent error classes
14
+ * participate without a package dependency.
15
+ *
16
+ * `SandboxExecTransportError` means both WebSocket attempts closed without an
17
+ * exit frame against a live sandbox. It only proves the command never started
18
+ * when the transport never opened (`opened: false` — the upgrade was refused
19
+ * outright). When the transport opened, the command may have run and mutated
20
+ * state before the result was lost, so replaying `git commit`, uploads, or
21
+ * arbitrary shell commands could execute the side effect twice; those errors
22
+ * surface to the caller instead.
23
+ */
24
+ export declare function isDeadSandboxError(error: unknown): boolean;
25
+ /**
26
+ * The local-provider equivalent of {@link isDeadSandboxError}: a local sandbox
27
+ * is just a directory, so it "dies" when that directory is removed — which
28
+ * session retirement does while an in-flight run still holds the handle.
29
+ *
30
+ * Node surfaces a missing `cwd` as ENOENT against the binary it tried to spawn
31
+ * (`spawn /bin/sh ENOENT`), which is textually identical to the shell itself
32
+ * being absent, and is also what a genuinely missing command reports. Probing
33
+ * the working directory is what separates "the sandbox is gone" from "that
34
+ * command does not exist", so only the former triggers a rebuild.
35
+ */
36
+ export declare function isMissingWorkdirError(error: unknown, workdir: string | undefined): boolean;
8
37
  export declare const FACTORY_SKILLS_SOURCE_PATH: string;
9
38
  export declare const FACTORY_SKILL_NAMES: Set<string>;
10
39
  type DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];
@@ -30,7 +59,7 @@ export declare class FactoryWorkspaceRegistry {
30
59
  register(sessionId: string, workspaceId: string, generation: number, unregister: WorkspaceUnregister): Promise<boolean>;
31
60
  invalidateSession(sessionId: string): Promise<void>;
32
61
  }
33
- export declare function createWorkspaceFactory(options?: CreateWorkspaceFactoryOptions): ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => Promise<Workspace<import("@mastra/core/workspace").WorkspaceFilesystem | undefined, import("@mastra/core/workspace").WorkspaceSandbox | undefined, undefined> | Workspace<import("@mastra/core/workspace").LocalFilesystem, LocalSandbox, undefined> | undefined>;
34
- export declare const getFactoryWorkspace: ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => Promise<Workspace<import("@mastra/core/workspace").WorkspaceFilesystem | undefined, import("@mastra/core/workspace").WorkspaceSandbox | undefined, undefined> | Workspace<import("@mastra/core/workspace").LocalFilesystem, LocalSandbox, undefined> | undefined>;
62
+ export declare function createWorkspaceFactory(options?: CreateWorkspaceFactoryOptions): ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => Promise<Workspace<import("@mastra/core/workspace").WorkspaceFilesystem | undefined, import("@mastra/core/workspace").WorkspaceSandbox | undefined, undefined> | Workspace<import("@mastra/core/workspace").LocalFilesystem, LocalSandbox, undefined> | Workspace<SandboxFilesystem, import("@mastra/core/workspace").WorkspaceSandbox, undefined> | undefined>;
63
+ export declare const getFactoryWorkspace: ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => Promise<Workspace<import("@mastra/core/workspace").WorkspaceFilesystem | undefined, import("@mastra/core/workspace").WorkspaceSandbox | undefined, undefined> | Workspace<import("@mastra/core/workspace").LocalFilesystem, LocalSandbox, undefined> | Workspace<SandboxFilesystem, import("@mastra/core/workspace").WorkspaceSandbox, undefined> | undefined>;
35
64
  export {};
36
65
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE,OAAO,EAAE,YAAY,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAc9E,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAK7E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AAID,eAAO,MAAM,0BAA0B,QAUS,CAAC;AAEjD,eAAO,MAAM,mBAAmB,aAO9B,CAAC;AA+DH,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,wEAAwE;IACxE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;IAC9D,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED,KAAK,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEtD,gFAAgF;AAChF,qBAAa,wBAAwB;;IAInC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAI/B,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAAC,OAAO,CAAC;IAWb,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IAkBlE,4CAA4C,uBAAuB,uQA4XlF;AAED,eAAO,MAAM,mBAAmB,+CA9X4B,uBAAuB,sQA8XxB,CAAC"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE,OAAO,EAAE,YAAY,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAgB9E,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAK7E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI1F;AAID,eAAO,MAAM,0BAA0B,QAUS,CAAC;AAEjD,eAAO,MAAM,mBAAmB,aAO9B,CAAC;AA6GH,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,wEAAwE;IACxE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;IAC9D,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED,KAAK,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEtD,gFAAgF;AAChF,qBAAa,wBAAwB;;IAInC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAI/B,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAAC,OAAO,CAAC;IAWb,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IA2BlE,4CAA4C,uBAAuB,oWAojBlF;AAED,eAAO,MAAM,mBAAmB,+CAtjB4B,uBAAuB,mWAsjBxB,CAAC"}
package/dist/workspace.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getFactoryAuthUserFromContext, getFactoryAuthUserId } from "./auth.js";
2
2
  import { getGithubPat } from "./integrations/github/pat.js";
3
3
  import { registerGithubPatKind, registerGithubTokenInjector } from "./integrations/github/token-refresh.js";
4
- import { DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, checkoutSessionBranch, materializeRepo, recycleClaimedWorkdir, runWorktreeSetup, runWorktreeTeardown } from "./integrations/github/sandbox.js";
4
+ import { DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, checkoutSessionBranch, hasExistingCheckout, materializeRepo, recycleClaimedWorkdir, runWorktreeSetup, runWorktreeTeardown } from "./integrations/github/sandbox.js";
5
+ import { baseCheckpointIsStale } from "./sandbox/base-checkpoint-triggers.js";
5
6
  import { getFactorySessionAddress } from "./rules/binding-context.js";
6
7
  import path, { dirname, join } from "path";
7
8
  import { LocalSandbox, LocalSkillSource, Workspace } from "@mastra/core/workspace";
@@ -17,6 +18,43 @@ const SESSION_CHECKPOINT_PREFIX = "mastracode-session";
17
18
  function checkpointNameForSession(sessionId) {
18
19
  return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;
19
20
  }
21
+ /**
22
+ * Whether a command failure means the sandbox itself is gone (destroyed by
23
+ * idle GC or provider teardown) AND the command provably never started, so
24
+ * reviving the sandbox and replaying the command cannot run a side effect
25
+ * twice. Matched by error name so any provider's equivalent error classes
26
+ * participate without a package dependency.
27
+ *
28
+ * `SandboxExecTransportError` means both WebSocket attempts closed without an
29
+ * exit frame against a live sandbox. It only proves the command never started
30
+ * when the transport never opened (`opened: false` — the upgrade was refused
31
+ * outright). When the transport opened, the command may have run and mutated
32
+ * state before the result was lost, so replaying `git commit`, uploads, or
33
+ * arbitrary shell commands could execute the side effect twice; those errors
34
+ * surface to the caller instead.
35
+ */
36
+ function isDeadSandboxError(error) {
37
+ if (!(error instanceof Error)) return false;
38
+ if (error.name === "SandboxDestroyedError") return true;
39
+ if (error.name === "SandboxExecTransportError") return error.opened === false;
40
+ return /sandbox .*(destroyed|no longer exists|not found)/i.test(error.message);
41
+ }
42
+ /**
43
+ * The local-provider equivalent of {@link isDeadSandboxError}: a local sandbox
44
+ * is just a directory, so it "dies" when that directory is removed — which
45
+ * session retirement does while an in-flight run still holds the handle.
46
+ *
47
+ * Node surfaces a missing `cwd` as ENOENT against the binary it tried to spawn
48
+ * (`spawn /bin/sh ENOENT`), which is textually identical to the shell itself
49
+ * being absent, and is also what a genuinely missing command reports. Probing
50
+ * the working directory is what separates "the sandbox is gone" from "that
51
+ * command does not exist", so only the former triggers a rebuild.
52
+ */
53
+ function isMissingWorkdirError(error, workdir) {
54
+ if (!workdir) return false;
55
+ if (error?.code !== "ENOENT") return false;
56
+ return !existsSync(workdir);
57
+ }
20
58
  const bundleDirectory = dirname(fileURLToPath(import.meta.url));
21
59
  const bundledFactorySkillsPath = join(bundleDirectory, "factory-skills");
22
60
  const FACTORY_SKILLS_SOURCE_PATH = [
@@ -68,6 +106,47 @@ var FactorySkillSource = class {
68
106
  return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);
69
107
  }
70
108
  };
109
+ function skillSourceEnoent(skillPath) {
110
+ const error = /* @__PURE__ */ new Error(`ENOENT: no such file or directory, '${skillPath}'`);
111
+ error.code = "ENOENT";
112
+ return error;
113
+ }
114
+ /**
115
+ * Sandbox-backed skill fallback that stays inert until the session sandbox is
116
+ * actually materialized. Skill discovery runs on latency-sensitive paths (the
117
+ * Factory start coordinator resolves the kickoff skill before the start route
118
+ * responds); without this guard the first project-root read would hit the lazy
119
+ * sandbox handle and force full provisioning + repo materialization. While the
120
+ * sandbox is unmaterialized, project skill roots simply appear empty — bundled
121
+ * Factory skills resolve from local disk via `FactorySkillSource`. Once the
122
+ * sandbox exists, every call delegates straight through.
123
+ */
124
+ var UnmaterializedAwareSkillSource = class {
125
+ fallback;
126
+ isMaterialized;
127
+ constructor(fallback, isMaterialized) {
128
+ this.fallback = fallback;
129
+ this.isMaterialized = isMaterialized;
130
+ }
131
+ async exists(skillPath) {
132
+ return this.isMaterialized() ? this.fallback.exists(skillPath) : false;
133
+ }
134
+ async stat(skillPath) {
135
+ if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);
136
+ return this.fallback.stat(skillPath);
137
+ }
138
+ async readFile(skillPath) {
139
+ if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);
140
+ return this.fallback.readFile(skillPath);
141
+ }
142
+ async readdir(skillPath) {
143
+ return this.isMaterialized() ? this.fallback.readdir(skillPath) : [];
144
+ }
145
+ realpath(skillPath) {
146
+ if (!this.isMaterialized()) return Promise.resolve(skillPath);
147
+ return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);
148
+ }
149
+ };
71
150
  const factorySkillExtension = {
72
151
  id: "web-factory",
73
152
  paths: [FACTORY_SKILLS_MOUNT],
@@ -106,6 +185,8 @@ function createWorkspaceFactory(options = {}) {
106
185
  const githubTokenInjectors = /* @__PURE__ */ new Map();
107
186
  const githubTokenReconciliations = /* @__PURE__ */ new Map();
108
187
  const inflightMaterializations = /* @__PURE__ */ new Map();
188
+ const materializedSandboxes = /* @__PURE__ */ new Map();
189
+ const constructedWorkspaces = /* @__PURE__ */ new Map();
109
190
  return async ({ requestContext, mastra, skillExtension }) => {
110
191
  const effectiveSkillExtension = skillExtension ?? factorySkillExtension;
111
192
  const ctx = requestContext.get("controller");
@@ -129,14 +210,13 @@ function createWorkspaceFactory(options = {}) {
129
210
  id: session.projectRepositoryId
130
211
  });
131
212
  if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);
132
- const connection = await storage.connections.get({
213
+ const [connection, repository] = await Promise.all([storage.connections.get({
133
214
  orgId: session.orgId,
134
215
  id: projectRepository.connectionId
135
- });
136
- const repository = await storage.repositories.get({
216
+ }), storage.repositories.get({
137
217
  orgId: session.orgId,
138
218
  id: projectRepository.repositoryId
139
- });
219
+ })]);
140
220
  if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);
141
221
  if (!await storage.installations.get({
142
222
  orgId: session.orgId,
@@ -144,15 +224,20 @@ function createWorkspaceFactory(options = {}) {
144
224
  })) throw new Error(`GitHub installation ${connection.installationId} was not found`);
145
225
  const repoFullName = repository.slug;
146
226
  let workdir = isLocalSandbox ? fleet.computeLocalSessionWorkdir(repoFullName, session.id) : session.sandboxWorkdir ?? projectRepository.sandboxWorkdir;
147
- if (ctx && workdir && ctx.getState()?.projectPath !== workdir) await ctx.setState({
148
- projectPath: workdir,
149
- projectName: repoFullName
150
- });
227
+ let stateSeeded = false;
228
+ if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {
229
+ await ctx.setState({
230
+ projectPath: workdir,
231
+ projectName: repoFullName
232
+ });
233
+ stateSeeded = true;
234
+ }
151
235
  const binding = {
152
236
  get sandboxId() {
153
237
  return session.sandboxId;
154
238
  },
155
239
  checkpointName: checkpointNameForSession(session.id),
240
+ ...!session.materializedAt && projectRepository.baseCheckpoint && !baseCheckpointIsStale(projectRepository) ? { seedCheckpointName: projectRepository.baseCheckpoint.name } : {},
156
241
  setSandboxId: async (id) => {
157
242
  await storage.sessions.setSandbox({
158
243
  id: session.id,
@@ -246,7 +331,11 @@ function createWorkspaceFactory(options = {}) {
246
331
  await workspace.destroy();
247
332
  evicted = true;
248
333
  } catch {}
249
- if (evicted && githubTokenInjectors.get(workspaceId) === registered) githubTokenInjectors.delete(workspaceId);
334
+ if (evicted && githubTokenInjectors.get(workspaceId) === registered) {
335
+ githubTokenInjectors.delete(workspaceId);
336
+ materializedSandboxes.delete(workspaceId);
337
+ constructedWorkspaces.delete(workspaceId);
338
+ }
250
339
  }
251
340
  throw error;
252
341
  }
@@ -256,12 +345,17 @@ function createWorkspaceFactory(options = {}) {
256
345
  let existing;
257
346
  try {
258
347
  existing = mastra?.getWorkspaceById(workspaceId);
259
- existing?.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);
260
348
  } catch {
261
349
  existing = void 0;
262
350
  }
263
- if (existing) return reconcileRegisteredWorkspace(existing);
264
- const materialize = async () => {
351
+ existing ??= constructedWorkspaces.get(workspaceId);
352
+ if (existing) {
353
+ existing.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);
354
+ return reconcileRegisteredWorkspace(existing);
355
+ }
356
+ const retiredError = () => /* @__PURE__ */ new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
357
+ const materializeSandbox = async () => {
358
+ if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw retiredError();
265
359
  let claimedPooledSandbox = false;
266
360
  if (!isLocalSandbox && !session.sandboxId) {
267
361
  const pooled = await storage.sandboxPool.claim({ projectRepositoryId: session.projectRepositoryId });
@@ -284,7 +378,7 @@ function createWorkspaceFactory(options = {}) {
284
378
  ...isLocalSandbox ? { workingDirectory: workdir } : {},
285
379
  actingUserId: userId
286
380
  });
287
- const runMaterialize = (target) => materializeRepo({
381
+ const runMaterialize = (target, skipPull) => materializeRepo({
288
382
  row: {
289
383
  id: session.id,
290
384
  sandboxWorkdir: workdir,
@@ -296,19 +390,21 @@ function createWorkspaceFactory(options = {}) {
296
390
  },
297
391
  sandbox: target,
298
392
  token,
299
- storage: storage.sessions
393
+ storage: storage.sessions,
394
+ skipPullOnExistingCheckout: skipPull
300
395
  });
301
396
  const isGitMissing = (error) => error instanceof MaterializeError && error.code === "git-missing";
302
397
  let sandbox = await ensureSandbox();
303
398
  if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);
399
+ const seededFromBaseCheckpoint = !!binding.seedCheckpointName && !session.materializedAt && !claimedPooledSandbox && await hasExistingCheckout(sandbox, workdir, repoFullName);
304
400
  try {
305
- await runMaterialize(sandbox);
401
+ await runMaterialize(sandbox, seededFromBaseCheckpoint);
306
402
  } catch (error) {
307
403
  if (!isGitMissing(error)) throw error;
308
404
  await fleet.teardownSandbox(binding, sandbox);
309
405
  sandbox = await ensureSandbox();
310
406
  try {
311
- await runMaterialize(sandbox);
407
+ await runMaterialize(sandbox, false);
312
408
  } catch (retryError) {
313
409
  if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);
314
410
  throw retryError;
@@ -320,7 +416,7 @@ function createWorkspaceFactory(options = {}) {
320
416
  token,
321
417
  repoFullName
322
418
  });
323
- if (projectRepository.setupCommand) try {
419
+ if (projectRepository.setupCommand && !seededFromBaseCheckpoint) try {
324
420
  await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);
325
421
  } catch (setupError) {
326
422
  if (projectRepository.teardownCommand) try {
@@ -346,47 +442,122 @@ function createWorkspaceFactory(options = {}) {
346
442
  generation: 0,
347
443
  tokenReplacementPending: false
348
444
  };
445
+ if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {
446
+ try {
447
+ await fleet.teardownSandbox(binding, sandbox);
448
+ } catch (teardownError) {
449
+ console.warn("[Mastra Factory] Sandbox teardown after mid-materialization retirement failed", {
450
+ orgId: session.orgId,
451
+ sessionId: session.sessionId,
452
+ error: teardownError instanceof Error ? teardownError.message.slice(-2e3) : String(teardownError)
453
+ });
454
+ }
455
+ throw retiredError();
456
+ }
349
457
  githubTokenInjectors.set(workspaceId, tokenRegistration);
350
458
  registerGithubTokenContext(tokenRegistration);
351
- const filesystem = new SandboxFilesystem({
352
- sandbox,
353
- workdir
354
- });
355
- const projectSkillPaths = [
356
- path.join(configDir, "skills"),
357
- ".claude/skills",
358
- ".agents/skills"
359
- ];
360
- const skillPaths = [...effectiveSkillExtension?.paths ?? [], ...projectSkillPaths];
361
- const workspace = new Workspace({
362
- id: workspaceId,
363
- name: "Mastra Code Factory Session Workspace",
364
- filesystem,
365
- sandbox,
366
- tools: MASTRACODE_WORKSPACE_TOOLS,
367
- skills: skillPaths,
368
- skillSource: effectiveSkillExtension?.createSource(filesystem, projectSkillPaths) ?? filesystem
369
- });
370
- mastra?.addWorkspace(workspace, workspaceId, { source: "mastra" });
371
- if (!await workspaceRegistry.register(session.sessionId, workspaceId, workspaceGeneration, async () => {
372
- githubTokenInjectors.delete(workspaceId);
373
- await mastra?.removeWorkspace?.(workspaceId);
374
- })) throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
375
- return workspace;
459
+ return sandbox;
376
460
  };
377
- const inflight = inflightMaterializations.get(workspaceId);
378
- if (inflight) return reconcileRegisteredWorkspace(await inflight);
379
- const materialization = materialize();
380
- inflightMaterializations.set(workspaceId, materialization);
381
- try {
382
- return await materialization;
383
- } finally {
384
- inflightMaterializations.delete(workspaceId);
385
- }
461
+ const ensureMaterialized = async () => {
462
+ const ready = materializedSandboxes.get(workspaceId);
463
+ if (ready) return ready;
464
+ let inflight = inflightMaterializations.get(workspaceId);
465
+ if (!inflight) {
466
+ inflight = materializeSandbox();
467
+ inflightMaterializations.set(workspaceId, inflight);
468
+ inflight.then((sb) => {
469
+ materializedSandboxes.set(workspaceId, sb);
470
+ workspace.skills?.refresh().catch(() => {});
471
+ }, () => {});
472
+ }
473
+ try {
474
+ return await inflight;
475
+ } finally {
476
+ if (inflightMaterializations.get(workspaceId) === inflight) inflightMaterializations.delete(workspaceId);
477
+ }
478
+ };
479
+ const lazySandbox = {
480
+ get id() {
481
+ return materializedSandboxes.get(workspaceId)?.id ?? workspaceId;
482
+ },
483
+ name: "Factory Lazy Sandbox",
484
+ get provider() {
485
+ return fleet.provider;
486
+ },
487
+ get status() {
488
+ return materializedSandboxes.has(workspaceId) ? "ready" : "pending";
489
+ },
490
+ get supportsCheckpoints() {
491
+ return materializedSandboxes.get(workspaceId)?.supportsCheckpoints ?? false;
492
+ },
493
+ getInstructions() {
494
+ return materializedSandboxes.get(workspaceId)?.getInstructions?.() ?? fleet.getInstructions();
495
+ },
496
+ clone() {
497
+ throw new Error("The Factory session sandbox cannot be cloned from a lazy handle.");
498
+ },
499
+ async start() {},
500
+ async getInfo() {
501
+ return (await ensureMaterialized()).getInfo();
502
+ },
503
+ async executeCommand(command, args, options) {
504
+ const sandbox = await ensureMaterialized();
505
+ try {
506
+ return await sandbox.executeCommand(command, args, options);
507
+ } catch (error) {
508
+ if (!isDeadSandboxError(error) && !(isLocalSandbox && isMissingWorkdirError(error, workdir))) throw error;
509
+ if (materializedSandboxes.get(workspaceId) === sandbox) materializedSandboxes.delete(workspaceId);
510
+ return (await ensureMaterialized()).executeCommand(command, args, options);
511
+ }
512
+ },
513
+ setEnvironmentVariable(name, value) {
514
+ const sandbox = materializedSandboxes.get(workspaceId);
515
+ if (!sandbox?.setEnvironmentVariable) throw new Error("The Factory session sandbox is not materialized yet.");
516
+ sandbox.setEnvironmentVariable(name, value);
517
+ },
518
+ async snapshot() {
519
+ await materializedSandboxes.get(workspaceId)?.snapshot?.();
520
+ },
521
+ async stop() {
522
+ await materializedSandboxes.get(workspaceId)?.stop?.();
523
+ }
524
+ };
525
+ const filesystem = new SandboxFilesystem({
526
+ id: `sandbox-fs:${workspaceId}:${workdir}`,
527
+ sandbox: lazySandbox,
528
+ workdir
529
+ });
530
+ const projectSkillPaths = [
531
+ path.join(configDir, "skills"),
532
+ ".claude/skills",
533
+ ".agents/skills"
534
+ ];
535
+ const guardedSkillFallback = new UnmaterializedAwareSkillSource(filesystem, () => materializedSandboxes.has(workspaceId));
536
+ const workspace = new Workspace({
537
+ id: workspaceId,
538
+ name: "Mastra Code Factory Session Workspace",
539
+ filesystem,
540
+ sandbox: lazySandbox,
541
+ tools: MASTRACODE_WORKSPACE_TOOLS,
542
+ skills: [...effectiveSkillExtension?.paths ?? [], ...projectSkillPaths],
543
+ skillSource: effectiveSkillExtension?.createSource(guardedSkillFallback, projectSkillPaths) ?? guardedSkillFallback
544
+ });
545
+ mastra?.addWorkspace(workspace, workspaceId, { source: "mastra" });
546
+ constructedWorkspaces.set(workspaceId, workspace);
547
+ if (!await workspaceRegistry.register(session.sessionId, workspaceId, workspaceGeneration, async () => {
548
+ githubTokenInjectors.delete(workspaceId);
549
+ materializedSandboxes.delete(workspaceId);
550
+ constructedWorkspaces.delete(workspaceId);
551
+ await mastra?.removeWorkspace?.(workspaceId);
552
+ })) throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
553
+ if (stateSeeded) ensureMaterialized().catch((error) => {
554
+ console.error(`[factory:workspace] background materialization for ${workspaceId} failed`, error);
555
+ });
556
+ return workspace;
386
557
  };
387
558
  }
388
559
  const getFactoryWorkspace = createWorkspaceFactory();
389
560
  //#endregion
390
- export { FACTORY_SKILLS_SOURCE_PATH, FACTORY_SKILL_NAMES, FactoryWorkspaceRegistry, checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace };
561
+ export { FACTORY_SKILLS_SOURCE_PATH, FACTORY_SKILL_NAMES, FactoryWorkspaceRegistry, checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace, isDeadSandboxError, isMissingWorkdirError };
391
562
 
392
563
  //# sourceMappingURL=workspace.js.map