@mastra/factory 0.24.0-alpha.11 → 0.24.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +25 -1
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +6 -3
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +3 -2
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.js +2 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +32 -30
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +5 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +87 -10
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +4 -2
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.js +4 -4
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +18 -0
- package/dist/session/resource-migration.d.ts.map +1 -1
- package/dist/session/resource-migration.js +24 -1
- package/dist/session/resource-migration.js.map +1 -1
- package/dist/session/session-threads.d.ts +9 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +24 -4
- package/dist/session/session-threads.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +15 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +29 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +2 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +201 -73
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +25 -10
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +26 -13
- package/dist/workspace.js.map +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -665,7 +665,7 @@ A person's own Factory session (a user session) gets tools to find, file and man
|
|
|
665
665
|
| `factory_retry_decision` | `decisionId` |
|
|
666
666
|
| `factory_dismiss_decision` | `decisionId` |
|
|
667
667
|
| `factory_resolve_proposal` | `decisionId`, `action` (`approve` or `dismiss`) |
|
|
668
|
-
| `factory_start_session` | `prompt` (up to 8000 characters), optional `title`; for a work-item role, `workItem` and `role`
|
|
668
|
+
| `factory_start_session` | `prompt` (up to 8000 characters), optional `title`, optional `model`; for a work-item role, `workItem` and `role` |
|
|
669
669
|
| `factory_answer_approval` | `sessionId`, `toolCallId`, `decision` (approve or decline), optional `reason` |
|
|
670
670
|
| `factory_switch_session_model` | `sessionId`, `modelId` |
|
|
671
671
|
| `factory_signal_session` | `sessionId`, `message` (up to 4000 characters), optional `priority` (`low`, `medium`, `high`, `urgent`; default `medium`) |
|
|
@@ -681,7 +681,7 @@ Read tools and `factory_message_origin` are available on every turn. The other t
|
|
|
681
681
|
|
|
682
682
|
`factory_signal_session` messages any other session in the same Factory that the caller can see: a work item's role session, or a teammate's session that isn't private. The message arrives as a `factory-session` notification on that session's own thread. Factory brings an offline session back online first, and the turn it wakes runs as that session's owner, with the owner's credentials, never as the sender.
|
|
683
683
|
|
|
684
|
-
How a message is delivered depends on its priority. `urgent` always delivers, even into a running turn. `high` and `medium` wake an idle session; while it is busy, `high` arrives as a summary and then in full, and `medium` waits for the current step. `low` arrives as a summary. Agents can read the full text of any notification with `notification_inbox`. A session can
|
|
684
|
+
How a message is delivered depends on its priority. `urgent` always delivers, even into a running turn. `high` and `medium` wake an idle session; while it is busy, `high` arrives as a summary and then in full, and `medium` waits for the current step. `low` arrives as a summary. Agents can read the full text of any notification with `notification_inbox`. A session can have at most 10 messages, replies and reports, together, delivered to the same session thread in 10 minutes. Past that, the tools queue the message and say when it will be delivered, so the sender does not send it again; it goes out, oldest first, once the window has a slot. `progress` reports don't count, since they replace each other.
|
|
685
685
|
|
|
686
686
|
`factory_message_origin` with `inReplyTo` answers a message. `inReplyTo` must name a `factory-session` message that reached the answering session's own thread, and the reply goes to the session and thread that sent it, never to an address the agent supplies. Each message can be answered once, and replies, reports and other notifications can't be answered, so two agents can't ping-pong. Replies arrive at `high` priority and carry an `in-reply-to` attribute. Messages and replies are recorded in the audit trail as `factory.agent.signaled`.
|
|
687
687
|
|
|
@@ -689,10 +689,10 @@ How a message is delivered depends on its priority. `urgent` always delivers, ev
|
|
|
689
689
|
|
|
690
690
|
`factory_start_session` starts another session for the caller and sends `prompt` as its first message. It has two modes:
|
|
691
691
|
|
|
692
|
-
- **Side session** (no `workItem`): a standalone session with no work item, for a one-off job. It uses the same repository as the session that started it. Factory stores that session on the new session's row (`origin_session_id`, `origin_thread_id`) and copies the parent's visibility.
|
|
692
|
+
- **Side session** (no `workItem`): a standalone session with no work item, for a one-off job. It uses the same repository as the session that started it. Factory stores that session on the new session's row (`origin_session_id`, `origin_thread_id`) and copies the parent's visibility. It runs on the model of the session that started it, or on `model` when one is passed. That model has to be one the session's credentials can run; an unknown or unrunnable model is refused before anything is created. When the parent has no model set, the side session keeps the Factory's default worker model.
|
|
693
693
|
- **Work-item role** (`workItem` and `role`): starts a role the item's board seats, the same way **Start** does in the Factory UI. The card does not move and no autonomy is armed. The tool refuses when the role already has a session, naming it so the agent can message it instead, and refuses an item linked to several repositories. The caller is subscribed to the item, and the run is recorded as `factory.run.started` with the caller as the person who started it.
|
|
694
694
|
|
|
695
|
-
Both modes attribute the new session to the caller, so it runs with the caller's credentials (the caller's own keys under the `user` model scope, the organization's under `factory`)
|
|
695
|
+
Both modes attribute the new session to the caller, so it runs with the caller's credentials (the caller's own keys under the `user` model scope, the organization's under `factory`). A work-item role keeps the Factory's default worker model. The first message runs with only the caller as its user, never the rest of the parent turn's context. The tool needs no approval but, like the other management tools, only appears on turns a signed-in person started.
|
|
696
696
|
|
|
697
697
|
#### Reports
|
|
698
698
|
|
|
@@ -49,8 +49,9 @@ var EnvironmentBuildRunner = class {
|
|
|
49
49
|
const project = await this.#deps.projects.getById({ id: projectId });
|
|
50
50
|
if (!project) return void 0;
|
|
51
51
|
const environment = await resolveProjectEnvironment(sourceControl.storage, project);
|
|
52
|
+
const heads = project.lastBuildHeads ?? project.activeTemplateHeads ?? await resolveEnvironmentHeads(sourceControl, project, environment);
|
|
52
53
|
return {
|
|
53
|
-
ctx: environmentBuildContext(project, environment,
|
|
54
|
+
ctx: environmentBuildContext(project, environment, heads, sourceControl.versionControl),
|
|
54
55
|
settings: environment.settings
|
|
55
56
|
};
|
|
56
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-runner.js","names":[
|
|
1
|
+
{"version":3,"file":"build-runner.js","names":[],"sources":["../../src/environment/build-runner.ts"],"sourcesContent":["/**\n * Starts `factory-environment-build` runs on the booted `Mastra`. Every\n * trigger goes through a workflow run so the start decision and the poll are\n * recorded the same way whether a route, the scheduler or a webhook asked.\n */\n\nimport type { Mastra } from '@mastra/core/mastra';\nimport type { FactorySandboxContext } from '@mastra/core/workspace';\nimport type { RepositoryPushEvent } from '../integrations/base.js';\nimport { resolveProjectEnvironment } from '../workspace.js';\nimport { probeSchedulesAvailable } from './build-schedule.js';\nimport type { ScheduleService } from './build-schedule.js';\nimport { createEnvironmentBuildWorkflow, ENVIRONMENT_BUILD_WORKFLOW_ID } from './build-workflow.js';\nimport type { EnvironmentBuildWorkflowOptions } from './build-workflow.js';\nimport type { EnvironmentBuildDeps, EnvironmentBuildOutcome, EnvironmentBuildTrigger } from './build.js';\nimport { environmentBuildContext, errorReason, resolveEnvironmentHeads } from './build.js';\n\nexport type StartEnvironmentBuild = (\n projectId: string,\n trigger: EnvironmentBuildTrigger,\n) => Promise<EnvironmentBuildOutcome>;\n\nexport interface EnvironmentBuildRunnerOptions extends Omit<EnvironmentBuildWorkflowOptions, 'onStart'> {\n /** The booted instance; `undefined` until the host finished `finalize()`. */\n getMastra: () => Mastra | undefined;\n logger?: { info: (msg: string, meta?: Record<string, unknown>) => void };\n}\n\n/** What a read of a build's status or history hands the provider. */\nexport interface EnvironmentBuildReadContext {\n ctx: FactorySandboxContext;\n settings: Record<string, unknown>;\n}\n\nexport class EnvironmentBuildRunner {\n readonly workflow;\n readonly #deps: EnvironmentBuildDeps;\n readonly #getMastra: () => Mastra | undefined;\n readonly #logger: EnvironmentBuildRunnerOptions['logger'];\n readonly #pending = new Map<string, (outcome: EnvironmentBuildOutcome) => void>();\n #scheduleAvailable = false;\n\n constructor(deps: EnvironmentBuildDeps, options: EnvironmentBuildRunnerOptions) {\n const { getMastra, logger, ...workflowOptions } = options;\n this.#deps = deps;\n this.#getMastra = getMastra;\n this.#logger = logger;\n this.workflow = createEnvironmentBuildWorkflow(deps, {\n ...workflowOptions,\n onStart: (runId, outcome) => {\n this.#pending.get(runId)?.(outcome);\n this.#pending.delete(runId);\n },\n });\n }\n\n /** The schedule service of the booted host, `undefined` before boot. */\n schedules(): ScheduleService | undefined {\n return this.#getMastra()?.schedules;\n }\n\n /** Whether the host's storage implements schedules; false until `probeSchedules()` ran. */\n get scheduleAvailable(): boolean {\n return this.#scheduleAvailable;\n }\n\n /** Probe once after the host booted; a storage without the domain disables the cron trigger. */\n async probeSchedules(): Promise<boolean> {\n const schedules = this.schedules();\n this.#scheduleAvailable = schedules ? await probeSchedulesAvailable(schedules) : false;\n return this.#scheduleAvailable;\n }\n\n /**\n * The context a status or history read hands the provider. Heads come from\n * the stored last attempt (the build being read was pinned to them), else\n * the active template, so a read never calls GitHub; before the first build\n * they are resolved live once.\n */\n async readContext(projectId: string): Promise<EnvironmentBuildReadContext | undefined> {\n const sourceControl = this.#deps.sourceControl;\n if (!sourceControl) return undefined;\n const project = await this.#deps.projects.getById({ id: projectId });\n if (!project) return undefined;\n const environment = await resolveProjectEnvironment(sourceControl.storage, project);\n const heads =\n project.lastBuildHeads ??\n project.activeTemplateHeads ??\n (await resolveEnvironmentHeads(sourceControl, project, environment));\n return {\n ctx: environmentBuildContext(project, environment, heads, sourceControl.versionControl),\n settings: environment.settings,\n };\n }\n\n /**\n * A push to an environment repository's default branch starts a `push` run\n * when the project opted in; the run's debounce claim decides whether it\n * builds. Fire and forget: the webhook never waits on it.\n */\n readonly onRepositoryPush = (event: RepositoryPushEvent): void => {\n if (!event.projectRepository.inEnvironment) return;\n const branch = event.defaultBranch;\n if (event.ref !== `refs/heads/${branch}`) return;\n void this.#deps.projects\n .getById({ id: event.factoryProjectId })\n .then(project => (project?.buildOnPushEnabled ? this.start(project.id, 'push') : undefined))\n .catch((error: unknown) => {\n this.#logger?.info('environment build push hand-off failed', {\n factoryProjectId: event.factoryProjectId,\n reason: errorReason(error),\n });\n });\n };\n\n /**\n * Start a run and resolve with the first step's answer; the poll step keeps\n * running in the background. Resolves `unavailable` before the host booted\n * or when the host never registered the workflow, and `failed` when the run\n * could not be created; a caller that already saved its own change must not\n * fail on the build it merely requested.\n */\n readonly start: StartEnvironmentBuild = async (projectId, trigger) => {\n const mastra = this.#getMastra();\n if (!mastra) return { outcome: 'unavailable', reason: 'not_ready' };\n let run: Awaited<ReturnType<typeof this.workflow.createRun>>;\n try {\n run = await mastra.getWorkflow(ENVIRONMENT_BUILD_WORKFLOW_ID).createRun();\n } catch (error) {\n const reason = errorReason(error);\n this.#logger?.info('environment build run could not be created', { factoryProjectId: projectId, reason });\n return mastra.listWorkflows()[ENVIRONMENT_BUILD_WORKFLOW_ID]\n ? { outcome: 'failed', reason }\n : { outcome: 'unavailable', reason: 'no_workflow' };\n }\n const outcome = new Promise<EnvironmentBuildOutcome>(resolve => this.#pending.set(run.runId, resolve));\n run\n .start({ inputData: { projectId, trigger } })\n .then(result => {\n if (result.status !== 'success') {\n this.#logger?.info('environment build run did not succeed', {\n factoryProjectId: projectId,\n runId: run.runId,\n });\n }\n })\n .catch((error: unknown) => {\n this.#logger?.info('environment build run threw', {\n factoryProjectId: projectId,\n runId: run.runId,\n reason: errorReason(error),\n });\n })\n .finally(() => {\n // A run that failed before its first step answered must not hang the caller.\n this.#pending.get(run.runId)?.({ outcome: 'failed', reason: 'The build run ended before it started a build.' });\n this.#pending.delete(run.runId);\n });\n return outcome;\n };\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
*/
|
|
14
14
|
const ENVIRONMENT_BUILD_WORKFLOW_ID = "factory-environment-build";
|
|
15
15
|
const DEFAULT_BUILD_POLL_MS = 15e3;
|
|
16
|
-
const MAX_BUILD_WAIT_MS =
|
|
16
|
+
const MAX_BUILD_WAIT_MS = 18e5;
|
|
17
17
|
const environmentBuildInputSchema = z.object({
|
|
18
18
|
projectId: z.string().min(1),
|
|
19
19
|
trigger: z.enum(ENVIRONMENT_BUILD_TRIGGERS)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-workflow.js","names":[],"sources":["../../src/environment/build-workflow.ts"],"sourcesContent":["/**\n * The `factory-environment-build` workflow: step one decides and starts a\n * build (`runEnvironmentBuild`), step two polls the provider until the build\n * settles and pins the active template on the project row. Registered by\n * `MastraFactory.prepare()` only when the sandbox has `builds`, and driven by\n * the Build now route, the per-project cron schedule and the GitHub push\n * hand-off. Saving settings never starts it.\n */\n\nimport { createStep, createWorkflow } from '@mastra/core/workflows';\nimport { z } from 'zod';\nimport { resolveProjectEnvironment } from '../workspace.js';\nimport {\n ENVIRONMENT_BUILD_TRIGGERS,\n environmentBuildContext,\n errorReason,\n redactCredentials,\n runEnvironmentBuild,\n} from './build.js';\nimport type { EnvironmentBuildDeps, EnvironmentBuildOutcome } from './build.js';\n\nexport const ENVIRONMENT_BUILD_WORKFLOW_ID = 'factory-environment-build';\nexport const DEFAULT_BUILD_POLL_MS = 15_000;\nexport const MAX_BUILD_WAIT_MS = 30 * 60_000;\n\nexport const environmentBuildInputSchema = z.object({\n projectId: z.string().min(1),\n trigger: z.enum(ENVIRONMENT_BUILD_TRIGGERS),\n});\n\nconst startOutputSchema = z.object({\n projectId: z.string(),\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n heads: z.record(z.string(), z.string()).optional(),\n});\n\nexport const environmentBuildOutputSchema = z.object({\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n /** Where the started build ended: `ready`, `failed`, or `unknown` when the provider lost it or the wait ran out. */\n status: z.enum(['ready', 'failed', 'unknown']).optional(),\n});\n\nexport type EnvironmentBuildWorkflowResult = z.infer<typeof environmentBuildOutputSchema>;\n\nexport interface EnvironmentBuildWorkflowOptions {\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n maxWaitMs?: number;\n /** Called with the first step's answer so a caller can return it without waiting for the poll. */\n onStart?: (runId: string, outcome: EnvironmentBuildOutcome) => void;\n}\n\nexport function createEnvironmentBuildWorkflow(\n deps: EnvironmentBuildDeps,\n options: EnvironmentBuildWorkflowOptions = {},\n) {\n const sleep = options.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)));\n const pollMs = options.pollMs ?? DEFAULT_BUILD_POLL_MS;\n const maxWaitMs = options.maxWaitMs ?? MAX_BUILD_WAIT_MS;\n const now = deps.now ?? (() => new Date());\n\n const start = createStep({\n id: 'start',\n inputSchema: environmentBuildInputSchema,\n outputSchema: startOutputSchema,\n execute: async ({ inputData, runId }) => {\n const outcome = await runEnvironmentBuild(deps, inputData);\n options.onStart?.(runId, outcome);\n return { projectId: inputData.projectId, ...outcome };\n },\n });\n\n const awaitBuild = createStep({\n id: 'await-build',\n inputSchema: startOutputSchema,\n outputSchema: environmentBuildOutputSchema,\n execute: async ({ inputData }): Promise<EnvironmentBuildWorkflowResult> => {\n const { projectId, heads, ...result } = inputData;\n if (result.outcome !== 'started' || !result.buildId || !heads) return result;\n const builds = deps.sandbox?.builds;\n const project = await deps.projects.getById({ id: projectId });\n if (!builds || !deps.sourceControl || !project) return { ...result, status: 'unknown' };\n const environment = await resolveProjectEnvironment(deps.sourceControl.storage, project);\n const ctx = environmentBuildContext(project, environment, heads, deps.sourceControl.versionControl);\n const startedAt = now().getTime();\n try {\n for (;;) {\n const build = await builds.get(ctx, environment.settings, result.buildId);\n if (build.status === 'ready') {\n const templateId = build.templateId ?? result.templateId ?? result.buildId;\n // A newer run may have replaced `last_build_id` while this one\n // polled; the pin is skipped then so the stale image never wins.\n const pinned = await deps.projects.pinActiveTemplate({\n id: project.id,\n buildId: result.buildId,\n templateId,\n heads,\n });\n deps.logger?.info(pinned ? 'environment build ready' : 'environment build ready but superseded', {\n factoryProjectId: project.id,\n templateId,\n });\n return { ...result, templateId, status: 'ready' };\n }\n if (build.status === 'failed' || build.status === 'unknown') {\n deps.logger?.info('environment build did not complete', {\n factoryProjectId: project.id,\n buildId: result.buildId,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n });\n return {\n ...result,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n };\n }\n if (now().getTime() - startedAt >= maxWaitMs) {\n return { ...result, status: 'unknown', reason: 'Build wait timed out.' };\n }\n await sleep(pollMs);\n }\n } catch (error) {\n const reason = errorReason(error);\n deps.logger?.info('environment build status read failed', { factoryProjectId: project.id, reason });\n return { ...result, status: 'unknown', reason };\n }\n },\n });\n\n return createWorkflow({\n id: ENVIRONMENT_BUILD_WORKFLOW_ID,\n inputSchema: environmentBuildInputSchema,\n outputSchema: environmentBuildOutputSchema,\n })\n .then(start)\n .then(awaitBuild)\n .commit();\n}\n"],"mappings":";;;;;;;;;;;;;AAqBA,MAAa,gCAAgC;AAC7C,MAAa,wBAAwB;AACrC,MAAa,oBAAoB
|
|
1
|
+
{"version":3,"file":"build-workflow.js","names":[],"sources":["../../src/environment/build-workflow.ts"],"sourcesContent":["/**\n * The `factory-environment-build` workflow: step one decides and starts a\n * build (`runEnvironmentBuild`), step two polls the provider until the build\n * settles and pins the active template on the project row. Registered by\n * `MastraFactory.prepare()` only when the sandbox has `builds`, and driven by\n * the Build now route, the per-project cron schedule and the GitHub push\n * hand-off. Saving settings never starts it.\n */\n\nimport { createStep, createWorkflow } from '@mastra/core/workflows';\nimport { z } from 'zod';\nimport { resolveProjectEnvironment } from '../workspace.js';\nimport {\n ENVIRONMENT_BUILD_TRIGGERS,\n environmentBuildContext,\n errorReason,\n redactCredentials,\n runEnvironmentBuild,\n} from './build.js';\nimport type { EnvironmentBuildDeps, EnvironmentBuildOutcome } from './build.js';\n\nexport const ENVIRONMENT_BUILD_WORKFLOW_ID = 'factory-environment-build';\nexport const DEFAULT_BUILD_POLL_MS = 15_000;\nexport const MAX_BUILD_WAIT_MS = 30 * 60_000;\n\nexport const environmentBuildInputSchema = z.object({\n projectId: z.string().min(1),\n trigger: z.enum(ENVIRONMENT_BUILD_TRIGGERS),\n});\n\nconst startOutputSchema = z.object({\n projectId: z.string(),\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n heads: z.record(z.string(), z.string()).optional(),\n});\n\nexport const environmentBuildOutputSchema = z.object({\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n /** Where the started build ended: `ready`, `failed`, or `unknown` when the provider lost it or the wait ran out. */\n status: z.enum(['ready', 'failed', 'unknown']).optional(),\n});\n\nexport type EnvironmentBuildWorkflowResult = z.infer<typeof environmentBuildOutputSchema>;\n\nexport interface EnvironmentBuildWorkflowOptions {\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n maxWaitMs?: number;\n /** Called with the first step's answer so a caller can return it without waiting for the poll. */\n onStart?: (runId: string, outcome: EnvironmentBuildOutcome) => void;\n}\n\nexport function createEnvironmentBuildWorkflow(\n deps: EnvironmentBuildDeps,\n options: EnvironmentBuildWorkflowOptions = {},\n) {\n const sleep = options.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)));\n const pollMs = options.pollMs ?? DEFAULT_BUILD_POLL_MS;\n const maxWaitMs = options.maxWaitMs ?? MAX_BUILD_WAIT_MS;\n const now = deps.now ?? (() => new Date());\n\n const start = createStep({\n id: 'start',\n inputSchema: environmentBuildInputSchema,\n outputSchema: startOutputSchema,\n execute: async ({ inputData, runId }) => {\n const outcome = await runEnvironmentBuild(deps, inputData);\n options.onStart?.(runId, outcome);\n return { projectId: inputData.projectId, ...outcome };\n },\n });\n\n const awaitBuild = createStep({\n id: 'await-build',\n inputSchema: startOutputSchema,\n outputSchema: environmentBuildOutputSchema,\n execute: async ({ inputData }): Promise<EnvironmentBuildWorkflowResult> => {\n const { projectId, heads, ...result } = inputData;\n if (result.outcome !== 'started' || !result.buildId || !heads) return result;\n const builds = deps.sandbox?.builds;\n const project = await deps.projects.getById({ id: projectId });\n if (!builds || !deps.sourceControl || !project) return { ...result, status: 'unknown' };\n const environment = await resolveProjectEnvironment(deps.sourceControl.storage, project);\n const ctx = environmentBuildContext(project, environment, heads, deps.sourceControl.versionControl);\n const startedAt = now().getTime();\n try {\n for (;;) {\n const build = await builds.get(ctx, environment.settings, result.buildId);\n if (build.status === 'ready') {\n const templateId = build.templateId ?? result.templateId ?? result.buildId;\n // A newer run may have replaced `last_build_id` while this one\n // polled; the pin is skipped then so the stale image never wins.\n const pinned = await deps.projects.pinActiveTemplate({\n id: project.id,\n buildId: result.buildId,\n templateId,\n heads,\n });\n deps.logger?.info(pinned ? 'environment build ready' : 'environment build ready but superseded', {\n factoryProjectId: project.id,\n templateId,\n });\n return { ...result, templateId, status: 'ready' };\n }\n if (build.status === 'failed' || build.status === 'unknown') {\n deps.logger?.info('environment build did not complete', {\n factoryProjectId: project.id,\n buildId: result.buildId,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n });\n return {\n ...result,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n };\n }\n if (now().getTime() - startedAt >= maxWaitMs) {\n return { ...result, status: 'unknown', reason: 'Build wait timed out.' };\n }\n await sleep(pollMs);\n }\n } catch (error) {\n const reason = errorReason(error);\n deps.logger?.info('environment build status read failed', { factoryProjectId: project.id, reason });\n return { ...result, status: 'unknown', reason };\n }\n },\n });\n\n return createWorkflow({\n id: ENVIRONMENT_BUILD_WORKFLOW_ID,\n inputSchema: environmentBuildInputSchema,\n outputSchema: environmentBuildOutputSchema,\n })\n .then(start)\n .then(awaitBuild)\n .commit();\n}\n"],"mappings":"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"}
|
|
@@ -86,7 +86,7 @@ async function resolveEnvironmentHeads(sourceControl, project, environment) {
|
|
|
86
86
|
* renews it every third of that while it works, so a lease lapses only when
|
|
87
87
|
* its run died or stalled.
|
|
88
88
|
*/
|
|
89
|
-
const BUILD_START_LEASE_MS =
|
|
89
|
+
const BUILD_START_LEASE_MS = 6e5;
|
|
90
90
|
/** Tries at recording a started build before the run leaves its lease to lapse. */
|
|
91
91
|
const RECORD_ATTEMPTS = 5;
|
|
92
92
|
/**
|
|
@@ -160,7 +160,7 @@ async function runEnvironmentBuild(deps, input) {
|
|
|
160
160
|
}).then((renewed) => {
|
|
161
161
|
if (!renewed) held = false;
|
|
162
162
|
}, () => {});
|
|
163
|
-
}, deps.leaseRenewMs ??
|
|
163
|
+
}, deps.leaseRenewMs ?? 2e5);
|
|
164
164
|
renewal.unref?.();
|
|
165
165
|
const attempted = { lastBuildAttemptedAt: now() };
|
|
166
166
|
let resolved = false;
|
|
@@ -203,23 +203,25 @@ async function runEnvironmentBuild(deps, input) {
|
|
|
203
203
|
heads = await resolveEnvironmentHeads(sourceControl, project, environment);
|
|
204
204
|
ctx = environmentBuildContext(project, environment, heads, sourceControl.versionControl);
|
|
205
205
|
buildInputs = environmentBuildInputs(environment);
|
|
206
|
-
if (claim.uncertain)
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
if (claim.uncertain) {
|
|
207
|
+
if (builds.list) {
|
|
208
|
+
const running = (await builds.list(ctx, environment.settings)).find((build) => build.status === "pending" || build.status === "building");
|
|
209
|
+
if (running) {
|
|
210
|
+
await release();
|
|
211
|
+
return {
|
|
212
|
+
outcome: "skipped",
|
|
213
|
+
reason: "in_progress",
|
|
214
|
+
buildId: running.buildId
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
resolved = true;
|
|
218
|
+
} else {
|
|
209
219
|
await release();
|
|
210
220
|
return {
|
|
211
221
|
outcome: "skipped",
|
|
212
|
-
reason: "
|
|
213
|
-
buildId: running.buildId
|
|
222
|
+
reason: "unresolved"
|
|
214
223
|
};
|
|
215
224
|
}
|
|
216
|
-
resolved = true;
|
|
217
|
-
} else {
|
|
218
|
-
await release();
|
|
219
|
-
return {
|
|
220
|
-
outcome: "skipped",
|
|
221
|
-
reason: "unresolved"
|
|
222
|
-
};
|
|
223
225
|
}
|
|
224
226
|
if (project.lastBuildId) {
|
|
225
227
|
const last = await builds.get(ctx, environment.settings, project.lastBuildId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","names":[],"sources":["../../src/environment/build.ts"],"sourcesContent":["/**\n * One proactive environment build: decide whether a trigger should build,\n * hand the sandbox's `builds` capability a context identical to a session's,\n * and record the started build on the project row. Polling the build to its\n * end and pinning the active template is the workflow's second step\n * (`build-workflow.ts`), so a request that only needs the start answer never\n * waits on the provider.\n */\n\nimport { createHash, randomUUID } from 'node:crypto';\n\nimport type { FactorySandbox, FactorySandboxContext } from '@mastra/core/workspace';\nimport type { VersionControl } from '../capabilities/version-control.js';\nimport type { FactoryProject, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { environmentSandboxContext, resolveProjectEnvironment } from '../workspace.js';\nimport type { SessionEnvironment } from '../workspace.js';\nimport { headsChanged, recordedHeadResolver, resolveCurrentHeads } from './heads.js';\nimport type { EnvironmentHeads } from './heads.js';\n\nexport const ENVIRONMENT_BUILD_TRIGGERS = ['manual', 'schedule', 'push'] as const;\nexport type EnvironmentBuildTrigger = (typeof ENVIRONMENT_BUILD_TRIGGERS)[number];\n\nexport interface EnvironmentBuildSourceControl {\n storage: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>;\n}\n\nexport interface EnvironmentBuildDeps {\n /** The factory's normalized sandbox; builds are unavailable without its `builds`. */\n sandbox: FactorySandbox | undefined;\n projects: Pick<\n FactoryProjectsStorage,\n 'getById' | 'claimBuildAttempt' | 'claimBuildStart' | 'renewBuildStart' | 'releaseBuildStart' | 'pinActiveTemplate'\n >;\n /** Absent when the factory has no source-control integration to resolve heads with. */\n sourceControl: EnvironmentBuildSourceControl | undefined;\n logger?: { info: (msg: string, meta?: Record<string, unknown>) => void };\n now?: () => Date;\n /** How often a run renews its build-start lease; a third of `BUILD_START_LEASE_MS` by default. */\n leaseRenewMs?: number;\n /** Waits between tries at recording a started build. */\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface EnvironmentBuildOutcome {\n outcome: 'started' | 'skipped' | 'unavailable' | 'failed';\n buildId?: string;\n templateId?: string;\n /** Why nothing started: `debounced`, `starting` (another run is starting a build), `in_progress`, `unchanged`, `unresolved` (a lapsed run may have started a build the provider cannot list), `no_environment`, `no_builds`, `no_source_control`; why a run failed: `not_recorded`, `superseded`, or an error. */\n reason?: string;\n /** The heads the started build pins; the workflow stores them once the build is ready. */\n heads?: EnvironmentHeads;\n}\n\n/** The synthetic session id a project's build runs under; never a session. */\nexport function environmentBuildSessionId(projectId: string): string {\n return `environment-build:${projectId}`;\n}\n\n/**\n * A build or setup error may echo a clone URL or a token; strip URL userinfo\n * and GitHub token shapes before the message is stored, logged or shown.\n */\nexport function redactCredentials(message: string): string {\n return message.replace(/\\/\\/[^/\\s@]+@/g, '//***@').replace(/\\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]+/g, '***');\n}\n\n/** An error's message with credentials redacted, safe to log, store or return. */\nexport function errorReason(error: unknown): string {\n return redactCredentials(error instanceof Error ? error.message : String(error));\n}\n\n/**\n * The context an environment build (or a read of its status) hands the\n * provider: the same list form a session boots with, under the build's own\n * session id, with heads served from `heads` so the provider never resolves\n * them itself.\n */\nexport function environmentBuildContext(\n project: Pick<FactoryProject, 'id' | 'orgId'>,\n environment: SessionEnvironment,\n heads: EnvironmentHeads,\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>,\n): FactorySandboxContext {\n return {\n sessionId: environmentBuildSessionId(project.id),\n ...environmentSandboxContext(environment, {\n orgId: project.orgId,\n getRepositoryAccess: args => versionControl.getRepositoryAccess(args),\n resolveHead: recordedHeadResolver(heads),\n }),\n };\n}\n\n/**\n * Fingerprint of everything a build bakes into the template besides the\n * repository heads: provider settings, the repositories and their order,\n * their setup commands, the workspace setup command and the working\n * directory. A schedule or push skips a ready build only while this matches.\n */\nexport function environmentBuildInputs(environment: SessionEnvironment): string {\n const inputs = {\n settings: environment.settings,\n workspaceSetupCommand: environment.workspaceSetupCommand ?? null,\n workingDirectory: environment.workingDirectory ?? null,\n repos: environment.repos.map(repo => ({\n repositoryId: repo.repositoryId,\n templateBranch: repo.templateBranch,\n setupCommand: repo.setupCommand ?? null,\n })),\n };\n return createHash('sha256').update(JSON.stringify(inputs)).digest('hex');\n}\n\n/**\n * The current head of every environment repository's template branch, keyed\n * by slug. The template clones the repository default branch (a link's own\n * branch is the session's base, checked out after boot), so that is the\n * branch a build pins; sessions let the provider resolve the same head.\n */\nexport async function resolveEnvironmentHeads(\n sourceControl: EnvironmentBuildSourceControl,\n project: Pick<FactoryProject, 'orgId'>,\n environment: SessionEnvironment,\n): Promise<EnvironmentHeads> {\n return resolveCurrentHeads(sourceControl, {\n orgId: project.orgId,\n repositories: environment.repos.map(repo => ({\n id: repo.repositoryId,\n slug: repo.slug,\n branch: repo.templateBranch,\n })),\n });\n}\n\n/**\n * How long a build-start lease lasts without renewal. The run holding it\n * renews it every third of that while it works, so a lease lapses only when\n * its run died or stalled.\n */\nexport const BUILD_START_LEASE_MS = 10 * 60_000;\n\n/** Tries at recording a started build before the run leaves its lease to lapse. */\nconst RECORD_ATTEMPTS = 5;\n\n/**\n * Start a build for `projectId` when `trigger` calls for one. Every trigger\n * takes the project's build-start lease before reading build state and renews\n * it until the started build is recorded, so overlapping manual, scheduled and\n * push runs never both start a build, however long the provider takes; a run\n * that finds the lease held skips as `starting`. Every trigger skips while the\n * last build is still running; `schedule` and `push` also skip when the last\n * build is ready and neither a head nor any other template input changed;\n * `manual` otherwise always builds. A `push` that would build then takes the\n * leading-edge debounce claim, so only a push that starts a build opens the\n * window. Saving settings never builds.\n *\n * Taking over a lapsed lease leaves the start uncertain: the lapsed run may\n * have started a build it never recorded. The mark persists until resolved.\n * While it stands, a provider that lists builds is asked for a running one\n * (and every trigger skips as `in_progress` on it); with a provider that\n * cannot list, every trigger skips as `unresolved`.\n *\n * The result is written as the lease is released, and only while this run\n * still holds it. A failed `builds.start` clears the last build id, so a later\n * `schedule` or `push` cannot mistake the previous ready build for the current\n * template; any other error keeps a running build's id. A started build whose\n * recording keeps failing yields `failed` (`not_recorded`) with the lease left\n * to lapse rather than released, and a run that lost its lease while the\n * provider started yields `failed` (`superseded`); neither is polled or pinned.\n */\nexport async function runEnvironmentBuild(\n deps: EnvironmentBuildDeps,\n input: { projectId: string; trigger: EnvironmentBuildTrigger },\n): Promise<EnvironmentBuildOutcome> {\n const builds = deps.sandbox?.builds;\n if (!builds) return { outcome: 'unavailable', reason: 'no_builds' };\n if (!deps.sourceControl) return { outcome: 'unavailable', reason: 'no_source_control' };\n const sourceControl = deps.sourceControl;\n const initial = await deps.projects.getById({ id: input.projectId });\n if (!initial) return { outcome: 'skipped', reason: 'no_environment' };\n const now = deps.now ?? (() => new Date());\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));\n\n const pushDebounced = (project: FactoryProject, at: Date) =>\n project.lastPushBuildAt !== null &&\n at.getTime() - project.lastPushBuildAt.getTime() < project.buildPushDebounceMinutes * 60_000;\n if (input.trigger === 'push' && pushDebounced(initial, now())) return { outcome: 'skipped', reason: 'debounced' };\n\n const token = randomUUID();\n const lease = { id: initial.id, token, leaseMs: BUILD_START_LEASE_MS };\n const claim = await deps.projects.claimBuildStart({ ...lease, now: now() });\n if (!claim.claimed) return { outcome: 'skipped', reason: 'starting' };\n let held = true;\n const renewal = setInterval(\n () => {\n deps.projects.renewBuildStart({ ...lease, now: now() }).then(\n renewed => {\n if (!renewed) held = false;\n },\n // A failed renewal is retried on the next tick; the lease outlasts two misses.\n () => {},\n );\n },\n deps.leaseRenewMs ?? BUILD_START_LEASE_MS / 3,\n );\n renewal.unref?.();\n\n const attempted = { lastBuildAttemptedAt: now() };\n // Set once this run has reconciled an uncertain start, or builds past it.\n let resolved = false;\n const release = (changes: Parameters<EnvironmentBuildDeps['projects']['releaseBuildStart']>[0]['changes'] = {}) =>\n deps.projects.releaseBuildStart({ id: initial.id, token, changes, resolved, now: now() });\n const failed = async (error: unknown, changes: Parameters<typeof release>[0]) => {\n const reason = errorReason(error);\n await release(changes);\n deps.logger?.info('environment build failed', { factoryProjectId: initial.id, trigger: input.trigger, reason });\n return { outcome: 'failed', reason } satisfies EnvironmentBuildOutcome;\n };\n\n try {\n let project: FactoryProject;\n let environment: SessionEnvironment;\n let heads: EnvironmentHeads;\n let ctx: FactorySandboxContext;\n let buildInputs: string;\n try {\n // Read build state only once this run holds the lease, so a run that\n // finished while this one waited has already recorded its build.\n project = (await deps.projects.getById({ id: initial.id })) ?? initial;\n environment = await resolveProjectEnvironment(sourceControl.storage, project);\n if (environment.repos.length === 0) {\n await release();\n return { outcome: 'skipped', reason: 'no_environment' };\n }\n heads = await resolveEnvironmentHeads(sourceControl, project, environment);\n ctx = environmentBuildContext(project, environment, heads, sourceControl.versionControl);\n buildInputs = environmentBuildInputs(environment);\n\n if (claim.uncertain) {\n // A run whose lease lapsed may have reached the provider before it\n // died; a build it left running is never raced by a second one. The\n // mark stays until that build is gone, since its inputs are unknown.\n if (builds.list) {\n const listed = await builds.list(ctx, environment.settings);\n const running = listed.find(build => build.status === 'pending' || build.status === 'building');\n if (running) {\n await release();\n return { outcome: 'skipped', reason: 'in_progress', buildId: running.buildId };\n }\n resolved = true;\n } else {\n // Without history the earlier start can never be reconciled, so no\n // trigger starts another build beside it.\n await release();\n return { outcome: 'skipped', reason: 'unresolved' };\n }\n }\n\n if (project.lastBuildId) {\n const last = await builds.get(ctx, environment.settings, project.lastBuildId);\n // Two runs polling one template fight over it (each poll resubmits its\n // own definition and the provider cancels the other), so a running\n // build is never joined by a second one, whatever asked.\n if (last.status === 'pending' || last.status === 'building') {\n await release();\n return { outcome: 'skipped', reason: 'in_progress', buildId: project.lastBuildId };\n }\n // A ready build whose poll ran out or died with its host was never\n // pinned; pin it now so the skip below compares against what it built.\n let pinnedHeads = project.activeTemplateHeads;\n const lastTemplateId = last.templateId ?? project.lastBuildId;\n if (last.status === 'ready' && project.lastBuildHeads && project.activeTemplateId !== lastTemplateId) {\n const pinned = await deps.projects.pinActiveTemplate({\n id: project.id,\n buildId: project.lastBuildId,\n templateId: lastTemplateId,\n heads: project.lastBuildHeads,\n });\n if (pinned) pinnedHeads = project.lastBuildHeads;\n }\n if (\n (input.trigger === 'schedule' || input.trigger === 'push') &&\n last.status === 'ready' &&\n !headsChanged(pinnedHeads, heads) &&\n project.lastBuildInputs === buildInputs\n ) {\n await release();\n return { outcome: 'skipped', reason: 'unchanged' };\n }\n }\n } catch (error) {\n return await failed(error, attempted);\n }\n\n // A run that lost its lease while reading must not start beside its successor.\n if (!held) return { outcome: 'skipped', reason: 'starting' };\n\n if (input.trigger === 'push') {\n // Claimed only now, so a push that skipped above never closes the window.\n let claimed: boolean;\n try {\n claimed = await deps.projects.claimBuildAttempt({\n id: project.id,\n debounceMinutes: project.buildPushDebounceMinutes,\n now: now(),\n });\n } catch (error) {\n return await failed(error, {});\n }\n if (!claimed) {\n await release();\n return { outcome: 'skipped', reason: 'debounced' };\n }\n }\n\n let started: Awaited<ReturnType<typeof builds.start>>;\n try {\n started = await builds.start(ctx, environment.settings);\n } catch (error) {\n // Only a failed start clears the last build: the previous one no longer\n // describes the template this project wants.\n return await failed(error, { lastBuildId: null, lastBuildHeads: null, lastBuildInputs: null, ...attempted });\n }\n\n // The provider has started the build: from here on, no error clears it.\n const log = { factoryProjectId: project.id, trigger: input.trigger, buildId: started.buildId };\n const changes = { lastBuildId: started.buildId, lastBuildHeads: heads, lastBuildInputs: buildInputs, ...attempted };\n let recorded: boolean | undefined;\n for (let attempt = 1; recorded === undefined; attempt++) {\n try {\n recorded = await release(changes);\n } catch (error) {\n if (attempt >= RECORD_ATTEMPTS) {\n // Keep the lease: it lapses once renewal stops, and the run that\n // takes it over looks for this build at the provider first.\n deps.logger?.info('environment build started but not recorded', { ...log, reason: errorReason(error) });\n return { outcome: 'failed', reason: 'not_recorded', buildId: started.buildId };\n }\n await sleep(1000 * 2 ** (attempt - 1));\n }\n }\n if (!recorded) {\n // A newer run took the lease while the provider started this build; the\n // project tracks that run's build, so this one is never polled or pinned.\n deps.logger?.info('environment build superseded', log);\n return { outcome: 'failed', reason: 'superseded', buildId: started.buildId };\n }\n try {\n deps.logger?.info('environment build started', { ...log, templateId: started.templateId });\n } catch {\n // Logging never turns a recorded build into a failure.\n }\n return {\n outcome: 'started',\n buildId: started.buildId,\n ...(started.templateId ? { templateId: started.templateId } : {}),\n heads,\n };\n } finally {\n clearInterval(renewal);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"build.js","names":[],"sources":["../../src/environment/build.ts"],"sourcesContent":["/**\n * One proactive environment build: decide whether a trigger should build,\n * hand the sandbox's `builds` capability a context identical to a session's,\n * and record the started build on the project row. Polling the build to its\n * end and pinning the active template is the workflow's second step\n * (`build-workflow.ts`), so a request that only needs the start answer never\n * waits on the provider.\n */\n\nimport { createHash, randomUUID } from 'node:crypto';\n\nimport type { FactorySandbox, FactorySandboxContext } from '@mastra/core/workspace';\nimport type { VersionControl } from '../capabilities/version-control.js';\nimport type { FactoryProject, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { environmentSandboxContext, resolveProjectEnvironment } from '../workspace.js';\nimport type { SessionEnvironment } from '../workspace.js';\nimport { headsChanged, recordedHeadResolver, resolveCurrentHeads } from './heads.js';\nimport type { EnvironmentHeads } from './heads.js';\n\nexport const ENVIRONMENT_BUILD_TRIGGERS = ['manual', 'schedule', 'push'] as const;\nexport type EnvironmentBuildTrigger = (typeof ENVIRONMENT_BUILD_TRIGGERS)[number];\n\nexport interface EnvironmentBuildSourceControl {\n storage: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>;\n}\n\nexport interface EnvironmentBuildDeps {\n /** The factory's normalized sandbox; builds are unavailable without its `builds`. */\n sandbox: FactorySandbox | undefined;\n projects: Pick<\n FactoryProjectsStorage,\n 'getById' | 'claimBuildAttempt' | 'claimBuildStart' | 'renewBuildStart' | 'releaseBuildStart' | 'pinActiveTemplate'\n >;\n /** Absent when the factory has no source-control integration to resolve heads with. */\n sourceControl: EnvironmentBuildSourceControl | undefined;\n logger?: { info: (msg: string, meta?: Record<string, unknown>) => void };\n now?: () => Date;\n /** How often a run renews its build-start lease; a third of `BUILD_START_LEASE_MS` by default. */\n leaseRenewMs?: number;\n /** Waits between tries at recording a started build. */\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface EnvironmentBuildOutcome {\n outcome: 'started' | 'skipped' | 'unavailable' | 'failed';\n buildId?: string;\n templateId?: string;\n /** Why nothing started: `debounced`, `starting` (another run is starting a build), `in_progress`, `unchanged`, `unresolved` (a lapsed run may have started a build the provider cannot list), `no_environment`, `no_builds`, `no_source_control`; why a run failed: `not_recorded`, `superseded`, or an error. */\n reason?: string;\n /** The heads the started build pins; the workflow stores them once the build is ready. */\n heads?: EnvironmentHeads;\n}\n\n/** The synthetic session id a project's build runs under; never a session. */\nexport function environmentBuildSessionId(projectId: string): string {\n return `environment-build:${projectId}`;\n}\n\n/**\n * A build or setup error may echo a clone URL or a token; strip URL userinfo\n * and GitHub token shapes before the message is stored, logged or shown.\n */\nexport function redactCredentials(message: string): string {\n return message.replace(/\\/\\/[^/\\s@]+@/g, '//***@').replace(/\\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]+/g, '***');\n}\n\n/** An error's message with credentials redacted, safe to log, store or return. */\nexport function errorReason(error: unknown): string {\n return redactCredentials(error instanceof Error ? error.message : String(error));\n}\n\n/**\n * The context an environment build (or a read of its status) hands the\n * provider: the same list form a session boots with, under the build's own\n * session id, with heads served from `heads` so the provider never resolves\n * them itself.\n */\nexport function environmentBuildContext(\n project: Pick<FactoryProject, 'id' | 'orgId'>,\n environment: SessionEnvironment,\n heads: EnvironmentHeads,\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>,\n): FactorySandboxContext {\n return {\n sessionId: environmentBuildSessionId(project.id),\n ...environmentSandboxContext(environment, {\n orgId: project.orgId,\n getRepositoryAccess: args => versionControl.getRepositoryAccess(args),\n resolveHead: recordedHeadResolver(heads),\n }),\n };\n}\n\n/**\n * Fingerprint of everything a build bakes into the template besides the\n * repository heads: provider settings, the repositories and their order,\n * their setup commands, the workspace setup command and the working\n * directory. A schedule or push skips a ready build only while this matches.\n */\nexport function environmentBuildInputs(environment: SessionEnvironment): string {\n const inputs = {\n settings: environment.settings,\n workspaceSetupCommand: environment.workspaceSetupCommand ?? null,\n workingDirectory: environment.workingDirectory ?? null,\n repos: environment.repos.map(repo => ({\n repositoryId: repo.repositoryId,\n templateBranch: repo.templateBranch,\n setupCommand: repo.setupCommand ?? null,\n })),\n };\n return createHash('sha256').update(JSON.stringify(inputs)).digest('hex');\n}\n\n/**\n * The current head of every environment repository's template branch, keyed\n * by slug. The template clones the repository default branch (a link's own\n * branch is the session's base, checked out after boot), so that is the\n * branch a build pins; sessions let the provider resolve the same head.\n */\nexport async function resolveEnvironmentHeads(\n sourceControl: EnvironmentBuildSourceControl,\n project: Pick<FactoryProject, 'orgId'>,\n environment: SessionEnvironment,\n): Promise<EnvironmentHeads> {\n return resolveCurrentHeads(sourceControl, {\n orgId: project.orgId,\n repositories: environment.repos.map(repo => ({\n id: repo.repositoryId,\n slug: repo.slug,\n branch: repo.templateBranch,\n })),\n });\n}\n\n/**\n * How long a build-start lease lasts without renewal. The run holding it\n * renews it every third of that while it works, so a lease lapses only when\n * its run died or stalled.\n */\nexport const BUILD_START_LEASE_MS = 10 * 60_000;\n\n/** Tries at recording a started build before the run leaves its lease to lapse. */\nconst RECORD_ATTEMPTS = 5;\n\n/**\n * Start a build for `projectId` when `trigger` calls for one. Every trigger\n * takes the project's build-start lease before reading build state and renews\n * it until the started build is recorded, so overlapping manual, scheduled and\n * push runs never both start a build, however long the provider takes; a run\n * that finds the lease held skips as `starting`. Every trigger skips while the\n * last build is still running; `schedule` and `push` also skip when the last\n * build is ready and neither a head nor any other template input changed;\n * `manual` otherwise always builds. A `push` that would build then takes the\n * leading-edge debounce claim, so only a push that starts a build opens the\n * window. Saving settings never builds.\n *\n * Taking over a lapsed lease leaves the start uncertain: the lapsed run may\n * have started a build it never recorded. The mark persists until resolved.\n * While it stands, a provider that lists builds is asked for a running one\n * (and every trigger skips as `in_progress` on it); with a provider that\n * cannot list, every trigger skips as `unresolved`.\n *\n * The result is written as the lease is released, and only while this run\n * still holds it. A failed `builds.start` clears the last build id, so a later\n * `schedule` or `push` cannot mistake the previous ready build for the current\n * template; any other error keeps a running build's id. A started build whose\n * recording keeps failing yields `failed` (`not_recorded`) with the lease left\n * to lapse rather than released, and a run that lost its lease while the\n * provider started yields `failed` (`superseded`); neither is polled or pinned.\n */\nexport async function runEnvironmentBuild(\n deps: EnvironmentBuildDeps,\n input: { projectId: string; trigger: EnvironmentBuildTrigger },\n): Promise<EnvironmentBuildOutcome> {\n const builds = deps.sandbox?.builds;\n if (!builds) return { outcome: 'unavailable', reason: 'no_builds' };\n if (!deps.sourceControl) return { outcome: 'unavailable', reason: 'no_source_control' };\n const sourceControl = deps.sourceControl;\n const initial = await deps.projects.getById({ id: input.projectId });\n if (!initial) return { outcome: 'skipped', reason: 'no_environment' };\n const now = deps.now ?? (() => new Date());\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));\n\n const pushDebounced = (project: FactoryProject, at: Date) =>\n project.lastPushBuildAt !== null &&\n at.getTime() - project.lastPushBuildAt.getTime() < project.buildPushDebounceMinutes * 60_000;\n if (input.trigger === 'push' && pushDebounced(initial, now())) return { outcome: 'skipped', reason: 'debounced' };\n\n const token = randomUUID();\n const lease = { id: initial.id, token, leaseMs: BUILD_START_LEASE_MS };\n const claim = await deps.projects.claimBuildStart({ ...lease, now: now() });\n if (!claim.claimed) return { outcome: 'skipped', reason: 'starting' };\n let held = true;\n const renewal = setInterval(\n () => {\n deps.projects.renewBuildStart({ ...lease, now: now() }).then(\n renewed => {\n if (!renewed) held = false;\n },\n // A failed renewal is retried on the next tick; the lease outlasts two misses.\n () => {},\n );\n },\n deps.leaseRenewMs ?? BUILD_START_LEASE_MS / 3,\n );\n renewal.unref?.();\n\n const attempted = { lastBuildAttemptedAt: now() };\n // Set once this run has reconciled an uncertain start, or builds past it.\n let resolved = false;\n const release = (changes: Parameters<EnvironmentBuildDeps['projects']['releaseBuildStart']>[0]['changes'] = {}) =>\n deps.projects.releaseBuildStart({ id: initial.id, token, changes, resolved, now: now() });\n const failed = async (error: unknown, changes: Parameters<typeof release>[0]) => {\n const reason = errorReason(error);\n await release(changes);\n deps.logger?.info('environment build failed', { factoryProjectId: initial.id, trigger: input.trigger, reason });\n return { outcome: 'failed', reason } satisfies EnvironmentBuildOutcome;\n };\n\n try {\n let project: FactoryProject;\n let environment: SessionEnvironment;\n let heads: EnvironmentHeads;\n let ctx: FactorySandboxContext;\n let buildInputs: string;\n try {\n // Read build state only once this run holds the lease, so a run that\n // finished while this one waited has already recorded its build.\n project = (await deps.projects.getById({ id: initial.id })) ?? initial;\n environment = await resolveProjectEnvironment(sourceControl.storage, project);\n if (environment.repos.length === 0) {\n await release();\n return { outcome: 'skipped', reason: 'no_environment' };\n }\n heads = await resolveEnvironmentHeads(sourceControl, project, environment);\n ctx = environmentBuildContext(project, environment, heads, sourceControl.versionControl);\n buildInputs = environmentBuildInputs(environment);\n\n if (claim.uncertain) {\n // A run whose lease lapsed may have reached the provider before it\n // died; a build it left running is never raced by a second one. The\n // mark stays until that build is gone, since its inputs are unknown.\n if (builds.list) {\n const listed = await builds.list(ctx, environment.settings);\n const running = listed.find(build => build.status === 'pending' || build.status === 'building');\n if (running) {\n await release();\n return { outcome: 'skipped', reason: 'in_progress', buildId: running.buildId };\n }\n resolved = true;\n } else {\n // Without history the earlier start can never be reconciled, so no\n // trigger starts another build beside it.\n await release();\n return { outcome: 'skipped', reason: 'unresolved' };\n }\n }\n\n if (project.lastBuildId) {\n const last = await builds.get(ctx, environment.settings, project.lastBuildId);\n // Two runs polling one template fight over it (each poll resubmits its\n // own definition and the provider cancels the other), so a running\n // build is never joined by a second one, whatever asked.\n if (last.status === 'pending' || last.status === 'building') {\n await release();\n return { outcome: 'skipped', reason: 'in_progress', buildId: project.lastBuildId };\n }\n // A ready build whose poll ran out or died with its host was never\n // pinned; pin it now so the skip below compares against what it built.\n let pinnedHeads = project.activeTemplateHeads;\n const lastTemplateId = last.templateId ?? project.lastBuildId;\n if (last.status === 'ready' && project.lastBuildHeads && project.activeTemplateId !== lastTemplateId) {\n const pinned = await deps.projects.pinActiveTemplate({\n id: project.id,\n buildId: project.lastBuildId,\n templateId: lastTemplateId,\n heads: project.lastBuildHeads,\n });\n if (pinned) pinnedHeads = project.lastBuildHeads;\n }\n if (\n (input.trigger === 'schedule' || input.trigger === 'push') &&\n last.status === 'ready' &&\n !headsChanged(pinnedHeads, heads) &&\n project.lastBuildInputs === buildInputs\n ) {\n await release();\n return { outcome: 'skipped', reason: 'unchanged' };\n }\n }\n } catch (error) {\n return await failed(error, attempted);\n }\n\n // A run that lost its lease while reading must not start beside its successor.\n if (!held) return { outcome: 'skipped', reason: 'starting' };\n\n if (input.trigger === 'push') {\n // Claimed only now, so a push that skipped above never closes the window.\n let claimed: boolean;\n try {\n claimed = await deps.projects.claimBuildAttempt({\n id: project.id,\n debounceMinutes: project.buildPushDebounceMinutes,\n now: now(),\n });\n } catch (error) {\n return await failed(error, {});\n }\n if (!claimed) {\n await release();\n return { outcome: 'skipped', reason: 'debounced' };\n }\n }\n\n let started: Awaited<ReturnType<typeof builds.start>>;\n try {\n started = await builds.start(ctx, environment.settings);\n } catch (error) {\n // Only a failed start clears the last build: the previous one no longer\n // describes the template this project wants.\n return await failed(error, { lastBuildId: null, lastBuildHeads: null, lastBuildInputs: null, ...attempted });\n }\n\n // The provider has started the build: from here on, no error clears it.\n const log = { factoryProjectId: project.id, trigger: input.trigger, buildId: started.buildId };\n const changes = { lastBuildId: started.buildId, lastBuildHeads: heads, lastBuildInputs: buildInputs, ...attempted };\n let recorded: boolean | undefined;\n for (let attempt = 1; recorded === undefined; attempt++) {\n try {\n recorded = await release(changes);\n } catch (error) {\n if (attempt >= RECORD_ATTEMPTS) {\n // Keep the lease: it lapses once renewal stops, and the run that\n // takes it over looks for this build at the provider first.\n deps.logger?.info('environment build started but not recorded', { ...log, reason: errorReason(error) });\n return { outcome: 'failed', reason: 'not_recorded', buildId: started.buildId };\n }\n await sleep(1000 * 2 ** (attempt - 1));\n }\n }\n if (!recorded) {\n // A newer run took the lease while the provider started this build; the\n // project tracks that run's build, so this one is never polled or pinned.\n deps.logger?.info('environment build superseded', log);\n return { outcome: 'failed', reason: 'superseded', buildId: started.buildId };\n }\n try {\n deps.logger?.info('environment build started', { ...log, templateId: started.templateId });\n } catch {\n // Logging never turns a recorded build into a failure.\n }\n return {\n outcome: 'started',\n buildId: started.buildId,\n ...(started.templateId ? { templateId: started.templateId } : {}),\n heads,\n };\n } finally {\n clearInterval(renewal);\n }\n}\n"],"mappings":"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"}
|
package/dist/factory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAiC,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAGlG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAkB,yBAAyB,EAAoB,MAAM,wBAAwB,CAAC;AAa1G,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAKvE,OAAO,KAAK,EAAE,kBAAkB,EAAgD,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAkBlF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"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"}
|
package/dist/factory.js
CHANGED
|
@@ -19,6 +19,7 @@ import { recordFactoryPullRequestProvenance, resolveFactoryPullRequestParentWork
|
|
|
19
19
|
import { dismissStaleFactoryReviews } from "./integrations/github/stale-reviews.js";
|
|
20
20
|
import { PlatformApiClient, platformApiClientConfigFromEnv } from "./integrations/platform/api-client.js";
|
|
21
21
|
import { buildPlatformConnectRoutes } from "./integrations/platform/connect/routes.js";
|
|
22
|
+
import { createSessionAccessGuard } from "./routes/session-access.js";
|
|
22
23
|
import { observeSessionFirstExec } from "./session/first-exec-capture.js";
|
|
23
24
|
import { observeSessionFilesystem } from "./session/filesystem-capture.js";
|
|
24
25
|
import { sessionMemoryResourceId, sessionStampCounts } from "./session/session-threads.js";
|
|
@@ -71,7 +72,7 @@ import { hydrateSessionDefaultModel } from "./session/default-model-hydration.js
|
|
|
71
72
|
import { observeSessionFirstMessage } from "./session/first-message-capture.js";
|
|
72
73
|
import { LiveSessions } from "./session/live-sessions.js";
|
|
73
74
|
import { FactoryReplyCredentialProcessor } from "./session/reply-credential-processor.js";
|
|
74
|
-
import { migrateSessionResources } from "./session/resource-migration.js";
|
|
75
|
+
import { migrateSessionResources, privatizeOwnerResourceSessions } from "./session/resource-migration.js";
|
|
75
76
|
import { createSourceControlTools } from "./session/source-control-tools.js";
|
|
76
77
|
import { readFactoryThinkingLevel, resolveFactoryThinkingLevel } from "./session/thinking-default.js";
|
|
77
78
|
import { observeSessionThreadStamp } from "./session/thread-session-stamp.js";
|
|
@@ -412,6 +413,11 @@ var MastraFactory = class {
|
|
|
412
413
|
threads: sessionMemory({ requestContext: new RequestContext() })
|
|
413
414
|
});
|
|
414
415
|
await sessionMemory.settled();
|
|
416
|
+
await privatizeOwnerResourceSessions({
|
|
417
|
+
sessions: sourceControlStorage,
|
|
418
|
+
workItems: workItemsStorage,
|
|
419
|
+
memory
|
|
420
|
+
});
|
|
415
421
|
} catch (error) {
|
|
416
422
|
console.warn(`[factory] session resource migration stopped early: ${error instanceof Error ? error.message : error}`);
|
|
417
423
|
}
|
|
@@ -1040,6 +1046,14 @@ var MastraFactory = class {
|
|
|
1040
1046
|
sandbox: sandboxConfig,
|
|
1041
1047
|
...environmentBuilds ? { onRepositoryPush: environmentBuilds.onRepositoryPush } : {},
|
|
1042
1048
|
sessionRetirement,
|
|
1049
|
+
moveThread: async ({ threadId, resourceId }) => {
|
|
1050
|
+
const sessionMemory = getDynamicMemory(storage.getMastraStorage(), vector);
|
|
1051
|
+
await sessionMemory({ requestContext: new RequestContext() }).updateThreadResourceId({
|
|
1052
|
+
threadId,
|
|
1053
|
+
resourceId
|
|
1054
|
+
});
|
|
1055
|
+
await sessionMemory.settled();
|
|
1056
|
+
},
|
|
1043
1057
|
factoryStorage: storage,
|
|
1044
1058
|
integrationStorage,
|
|
1045
1059
|
sourceControlStorage,
|
|
@@ -1146,6 +1160,16 @@ var MastraFactory = class {
|
|
|
1146
1160
|
middleware: [
|
|
1147
1161
|
...timing,
|
|
1148
1162
|
createFactoryAuthGate(auth),
|
|
1163
|
+
createSessionAccessGuard({
|
|
1164
|
+
auth: routeAuth,
|
|
1165
|
+
sessions: sourceControlSessions,
|
|
1166
|
+
projects: { get: async (args) => {
|
|
1167
|
+
if (!storage.isDomainReady("projects")) return null;
|
|
1168
|
+
await factoryProjectsStorage.ensureReady();
|
|
1169
|
+
return factoryProjectsStorage.get(args);
|
|
1170
|
+
} },
|
|
1171
|
+
controller: () => this.#prepared?.base.controller
|
|
1172
|
+
}),
|
|
1149
1173
|
createTenantCredentialPrimer({
|
|
1150
1174
|
auth: routeAuth,
|
|
1151
1175
|
credentials: modelCredentialsStorage
|