@mastra/factory 0.8.0-alpha.8 → 0.8.0
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/CHANGELOG.md +660 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +42 -4
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +8 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.d.ts +43 -0
- package/dist/integrations/github/app-identity.d.ts.map +1 -0
- package/dist/integrations/github/app-identity.js +62 -0
- package/dist/integrations/github/app-identity.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +9 -0
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +9 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.d.ts +5 -0
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +7 -0
- 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 +11 -6
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +8 -0
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +30 -8
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +39 -6
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +48 -14
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +39 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +74 -13
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +1 -0
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts +10 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +35 -1
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +2 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.d.ts +11 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +64 -16
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +12 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +47 -4
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +1 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +2 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/config.d.ts +12 -6
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +97 -56
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +2 -0
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.d.ts +15 -0
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +65 -2
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.d.ts +152 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +442 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/projects.d.ts +9 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +8 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/surface.d.ts +6 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -0
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +3 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +4 -1
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +87 -4
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +6 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +118 -16
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +20 -7
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.d.ts +1 -0
- package/dist/rules/resolve.d.ts.map +1 -1
- package/dist/rules/resolve.js +3 -2
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +2 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +23 -0
- package/dist/rules/terminal-cleanup.d.ts.map +1 -0
- package/dist/rules/terminal-cleanup.js +43 -0
- package/dist/rules/terminal-cleanup.js.map +1 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +24 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +3 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +10 -6
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +32 -3
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +8 -3
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/base-checkpoint-triggers.d.ts +77 -0
- package/dist/sandbox/base-checkpoint-triggers.d.ts.map +1 -0
- package/dist/sandbox/base-checkpoint-triggers.js +137 -0
- package/dist/sandbox/base-checkpoint-triggers.js.map +1 -0
- package/dist/sandbox/base-checkpoint.d.ts +59 -0
- package/dist/sandbox/base-checkpoint.d.ts.map +1 -0
- package/dist/sandbox/base-checkpoint.js +145 -0
- package/dist/sandbox/base-checkpoint.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +32 -0
- package/dist/sandbox/fleet.d.ts.map +1 -1
- package/dist/sandbox/fleet.js +39 -10
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/session/checkpoint-capture.d.ts +3 -2
- package/dist/session/checkpoint-capture.d.ts.map +1 -1
- package/dist/session/checkpoint-capture.js +3 -3
- package/dist/session/checkpoint-capture.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +2 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +7 -0
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +41 -4
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/model-pack-hydration.d.ts +55 -0
- package/dist/session/model-pack-hydration.d.ts.map +1 -0
- package/dist/session/model-pack-hydration.js +56 -0
- package/dist/session/model-pack-hydration.js.map +1 -0
- package/dist/skills/service.d.ts +9 -0
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +10 -1
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +36 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +91 -2
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.d.ts +26 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -1
- package/dist/storage/domains/model-packs/base.js +102 -13
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +47 -1
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +78 -19
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +11 -3
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +10 -1
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +67 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +183 -12
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/workspace.d.ts +31 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +225 -54
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-plan/SKILL.md +1 -1
- package/factory-skills/factory-rereview/SKILL.md +3 -3
- package/factory-skills/factory-review/SKILL.md +3 -3
- package/factory-skills/factory-triage/SKILL.md +2 -0
- package/package.json +8 -8
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { materializeRepo, runWorktreeSetup, sh, shellQuote } from "../integrations/github/sandbox.js";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
//#region src/sandbox/base-checkpoint.ts
|
|
4
|
+
/**
|
|
5
|
+
* Base-checkpoint build job.
|
|
6
|
+
*
|
|
7
|
+
* Keeps a continuously-refreshed "base checkpoint" per connected repo: a
|
|
8
|
+
* sandbox filesystem with the default branch already cloned and the repo's
|
|
9
|
+
* setup command already run, snapshotted under `repo-<projectRepositoryId>`.
|
|
10
|
+
* New sessions boot from this checkpoint and only pay `git fetch` + branch
|
|
11
|
+
* checkout instead of a cold clone + install.
|
|
12
|
+
*
|
|
13
|
+
* Builds are triggered on repo connect, merges/pushes to the default branch,
|
|
14
|
+
* and the reconcile sweep. Rapid triggers coalesce: only one build runs per
|
|
15
|
+
* repo at a time, and a trigger arriving mid-build marks it dirty so exactly
|
|
16
|
+
* one follow-up build runs afterwards.
|
|
17
|
+
*
|
|
18
|
+
* Providers without real checkpoint support (capability-driven via
|
|
19
|
+
* `supportsCheckpoints`) skip the snapshot — sessions keep using the cold
|
|
20
|
+
* path. A failed build never blocks sessions either; it only logs, and the
|
|
21
|
+
* reconcile sweep retries later.
|
|
22
|
+
*/
|
|
23
|
+
/** Provider checkpoint name for a repo's warm base image. */
|
|
24
|
+
function baseCheckpointName(projectRepositoryId) {
|
|
25
|
+
return `repo-${projectRepositoryId}`;
|
|
26
|
+
}
|
|
27
|
+
/** Stable hash of the setup command, used to invalidate stale checkpoints. */
|
|
28
|
+
function hashSetupCommand(setupCommand) {
|
|
29
|
+
if (!setupCommand) return null;
|
|
30
|
+
return createHash("sha256").update(setupCommand).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Coalescing runner for base-checkpoint builds. One instance per server;
|
|
34
|
+
* `request()` is fire-and-forget safe (never throws).
|
|
35
|
+
*/
|
|
36
|
+
var BaseCheckpointBuilder = class {
|
|
37
|
+
#fleet;
|
|
38
|
+
#logger;
|
|
39
|
+
/** In-flight build per projectRepositoryId. */
|
|
40
|
+
#inflight = /* @__PURE__ */ new Map();
|
|
41
|
+
/** Repos re-triggered mid-build: run exactly one follow-up build. */
|
|
42
|
+
#dirty = /* @__PURE__ */ new Map();
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.#fleet = options.fleet;
|
|
45
|
+
this.#logger = options.logger;
|
|
46
|
+
}
|
|
47
|
+
/** True when a build for this repo is currently running. */
|
|
48
|
+
isBuilding(projectRepositoryId) {
|
|
49
|
+
return this.#inflight.has(projectRepositoryId);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Request a (re)build of the repo's base checkpoint. Coalesces with any
|
|
53
|
+
* in-flight build for the same repo. Resolves when this request's build
|
|
54
|
+
* (or the follow-up build it folded into) settles. Never rejects.
|
|
55
|
+
*/
|
|
56
|
+
request(job) {
|
|
57
|
+
const key = job.projectRepositoryId;
|
|
58
|
+
const existing = this.#inflight.get(key);
|
|
59
|
+
if (existing) {
|
|
60
|
+
this.#dirty.set(key, job);
|
|
61
|
+
return existing.then(() => {
|
|
62
|
+
return this.#inflight.get(key) ?? Promise.resolve();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const run = this.#buildSafely(job).then(() => {
|
|
66
|
+
this.#inflight.delete(key);
|
|
67
|
+
const followUp = this.#dirty.get(key);
|
|
68
|
+
if (followUp) {
|
|
69
|
+
this.#dirty.delete(key);
|
|
70
|
+
return this.request(followUp);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.#inflight.set(key, run);
|
|
74
|
+
return run;
|
|
75
|
+
}
|
|
76
|
+
/** One build attempt; logs failures instead of throwing. */
|
|
77
|
+
async #buildSafely(job) {
|
|
78
|
+
try {
|
|
79
|
+
await this.#build(job);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.#logger?.warn?.(`Base-checkpoint build failed for ${job.repoFullName} (${job.projectRepositoryId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async #build(job) {
|
|
85
|
+
if (!this.#fleet.enabled) return;
|
|
86
|
+
const checkpointName = baseCheckpointName(job.projectRepositoryId);
|
|
87
|
+
let boundSandboxId = null;
|
|
88
|
+
const binding = {
|
|
89
|
+
get sandboxId() {
|
|
90
|
+
return boundSandboxId;
|
|
91
|
+
},
|
|
92
|
+
checkpointName,
|
|
93
|
+
setSandboxId: async (id) => {
|
|
94
|
+
boundSandboxId = id;
|
|
95
|
+
},
|
|
96
|
+
clear: async () => {
|
|
97
|
+
boundSandboxId = null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const token = await job.getToken();
|
|
101
|
+
let sandbox;
|
|
102
|
+
try {
|
|
103
|
+
sandbox = await this.#fleet.ensureSandbox(binding, { GH_TOKEN: token }, void 0, {
|
|
104
|
+
workingDirectory: job.workdir,
|
|
105
|
+
...job.actingUserId ? { actingUserId: job.actingUserId } : {}
|
|
106
|
+
});
|
|
107
|
+
if (!sandbox.supportsCheckpoints || !sandbox.snapshot) return;
|
|
108
|
+
await materializeRepo({
|
|
109
|
+
row: {
|
|
110
|
+
id: job.projectRepositoryId,
|
|
111
|
+
sandboxWorkdir: job.workdir,
|
|
112
|
+
materializedAt: null
|
|
113
|
+
},
|
|
114
|
+
repoInfo: {
|
|
115
|
+
repoFullName: job.repoFullName,
|
|
116
|
+
defaultBranch: job.defaultBranch
|
|
117
|
+
},
|
|
118
|
+
sandbox,
|
|
119
|
+
token,
|
|
120
|
+
storage: { markMaterialized: async () => {} }
|
|
121
|
+
});
|
|
122
|
+
if (job.setupCommand) await runWorktreeSetup(sandbox, job.workdir, job.setupCommand);
|
|
123
|
+
const head = await sh(sandbox, `git -C ${shellQuote(job.workdir)} rev-parse HEAD`);
|
|
124
|
+
if (head.exitCode !== 0) throw new Error(`Failed to read HEAD after materialize: ${head.stderr}`);
|
|
125
|
+
const sha = head.stdout.trim();
|
|
126
|
+
await sandbox.snapshot();
|
|
127
|
+
await job.storage.projectRepositories.setBaseCheckpoint({
|
|
128
|
+
id: job.projectRepositoryId,
|
|
129
|
+
checkpoint: {
|
|
130
|
+
name: checkpointName,
|
|
131
|
+
sha,
|
|
132
|
+
builtAt: /* @__PURE__ */ new Date(),
|
|
133
|
+
setupCommandHash: hashSetupCommand(job.setupCommand)
|
|
134
|
+
},
|
|
135
|
+
expectedSetupCommand: job.setupCommand
|
|
136
|
+
});
|
|
137
|
+
} finally {
|
|
138
|
+
await this.#fleet.teardownSandbox(binding, sandbox).catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
export { BaseCheckpointBuilder, baseCheckpointName, hashSetupCommand };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=base-checkpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-checkpoint.js","names":["#fleet","#logger","#inflight","#dirty","#buildSafely","#build"],"sources":["../../src/sandbox/base-checkpoint.ts"],"sourcesContent":["/**\n * Base-checkpoint build job.\n *\n * Keeps a continuously-refreshed \"base checkpoint\" per connected repo: a\n * sandbox filesystem with the default branch already cloned and the repo's\n * setup command already run, snapshotted under `repo-<projectRepositoryId>`.\n * New sessions boot from this checkpoint and only pay `git fetch` + branch\n * checkout instead of a cold clone + install.\n *\n * Builds are triggered on repo connect, merges/pushes to the default branch,\n * and the reconcile sweep. Rapid triggers coalesce: only one build runs per\n * repo at a time, and a trigger arriving mid-build marks it dirty so exactly\n * one follow-up build runs afterwards.\n *\n * Providers without real checkpoint support (capability-driven via\n * `supportsCheckpoints`) skip the snapshot — sessions keep using the cold\n * path. A failed build never blocks sessions either; it only logs, and the\n * reconcile sweep retries later.\n */\n\nimport { createHash } from 'node:crypto';\nimport type { IMastraLogger } from '@mastra/core/logger';\nimport { materializeRepo, runWorktreeSetup, sh, shellQuote } from '../integrations/github/sandbox.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { MaterializationSandbox, SandboxBindingStore, SandboxFleet } from './fleet.js';\n\n/** Provider checkpoint name for a repo's warm base image. */\nexport function baseCheckpointName(projectRepositoryId: string): string {\n return `repo-${projectRepositoryId}`;\n}\n\n/** Stable hash of the setup command, used to invalidate stale checkpoints. */\nexport function hashSetupCommand(setupCommand: string | null): string | null {\n if (!setupCommand) return null;\n return createHash('sha256').update(setupCommand).digest('hex');\n}\n\n/** Everything one build run needs; resolved by the trigger before enqueueing. */\nexport interface BaseCheckpointJob {\n projectRepositoryId: string;\n repoFullName: string;\n defaultBranch: string;\n setupCommand: string | null;\n /** Workdir the clone lives at — must match what sessions use. */\n workdir: string;\n /** Mint a fresh short-lived installation token for git operations. */\n getToken(): Promise<string>;\n storage: SourceControlStorageHandle;\n actingUserId?: string;\n}\n\n/**\n * Coalescing runner for base-checkpoint builds. One instance per server;\n * `request()` is fire-and-forget safe (never throws).\n */\nexport class BaseCheckpointBuilder {\n readonly #fleet: SandboxFleet;\n readonly #logger?: IMastraLogger;\n /** In-flight build per projectRepositoryId. */\n readonly #inflight = new Map<string, Promise<void>>();\n /** Repos re-triggered mid-build: run exactly one follow-up build. */\n readonly #dirty = new Map<string, BaseCheckpointJob>();\n\n constructor(options: { fleet: SandboxFleet; logger?: IMastraLogger }) {\n this.#fleet = options.fleet;\n this.#logger = options.logger;\n }\n\n /** True when a build for this repo is currently running. */\n isBuilding(projectRepositoryId: string): boolean {\n return this.#inflight.has(projectRepositoryId);\n }\n\n /**\n * Request a (re)build of the repo's base checkpoint. Coalesces with any\n * in-flight build for the same repo. Resolves when this request's build\n * (or the follow-up build it folded into) settles. Never rejects.\n */\n request(job: BaseCheckpointJob): Promise<void> {\n const key = job.projectRepositoryId;\n const existing = this.#inflight.get(key);\n if (existing) {\n // Fold into the in-flight build: remember the latest job so one\n // follow-up rebuild runs with fresh inputs once the current one ends.\n this.#dirty.set(key, job);\n return existing.then(() => {\n const pending = this.#inflight.get(key);\n return pending ?? Promise.resolve();\n });\n }\n\n const run = this.#buildSafely(job).then(() => {\n this.#inflight.delete(key);\n const followUp = this.#dirty.get(key);\n if (followUp) {\n this.#dirty.delete(key);\n return this.request(followUp);\n }\n });\n this.#inflight.set(key, run);\n return run;\n }\n\n /** One build attempt; logs failures instead of throwing. */\n async #buildSafely(job: BaseCheckpointJob): Promise<void> {\n try {\n await this.#build(job);\n } catch (error) {\n this.#logger?.warn?.(\n `Base-checkpoint build failed for ${job.repoFullName} (${job.projectRepositoryId}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n }\n\n async #build(job: BaseCheckpointJob): Promise<void> {\n if (!this.#fleet.enabled) return;\n\n const checkpointName = baseCheckpointName(job.projectRepositoryId);\n // Ephemeral binding: the builder VM is never reattached to — it exists\n // only to produce the checkpoint. Incremental rebuilds still happen\n // because the provider seeds the fresh VM from the previous checkpoint\n // (same `checkpointName`), so materializeRepo pulls instead of cloning.\n let boundSandboxId: string | null = null;\n const binding: SandboxBindingStore = {\n get sandboxId() {\n return boundSandboxId;\n },\n checkpointName,\n setSandboxId: async id => {\n boundSandboxId = id;\n },\n clear: async () => {\n boundSandboxId = null;\n },\n };\n\n const token = await job.getToken();\n let sandbox: MaterializationSandbox | undefined;\n try {\n sandbox = await this.#fleet.ensureSandbox(binding, { GH_TOKEN: token }, undefined, {\n workingDirectory: job.workdir,\n ...(job.actingUserId ? { actingUserId: job.actingUserId } : {}),\n });\n\n if (!sandbox.supportsCheckpoints || !sandbox.snapshot) {\n // No real checkpoint support — nothing to build; sessions use the\n // existing cold path.\n return;\n }\n\n // Clone (cold) or pull (VM seeded from the previous checkpoint).\n await materializeRepo({\n row: { id: job.projectRepositoryId, sandboxWorkdir: job.workdir, materializedAt: null },\n repoInfo: { repoFullName: job.repoFullName, defaultBranch: job.defaultBranch },\n sandbox,\n token,\n storage: { markMaterialized: async () => {} },\n });\n\n if (job.setupCommand) {\n await runWorktreeSetup(sandbox, job.workdir, job.setupCommand);\n }\n\n const head = await sh(sandbox, `git -C ${shellQuote(job.workdir)} rev-parse HEAD`);\n if (head.exitCode !== 0) {\n throw new Error(`Failed to read HEAD after materialize: ${head.stderr}`);\n }\n const sha = head.stdout.trim();\n\n await sandbox.snapshot();\n\n await job.storage.projectRepositories.setBaseCheckpoint({\n id: job.projectRepositoryId,\n checkpoint: {\n name: checkpointName,\n sha,\n builtAt: new Date(),\n setupCommandHash: hashSetupCommand(job.setupCommand),\n },\n expectedSetupCommand: job.setupCommand,\n });\n } finally {\n // Best-effort teardown: the builder VM is single-use. Route cleanup\n // through the fleet so the live-sandbox budget is released as well.\n await this.#fleet.teardownSandbox(binding, sandbox).catch(() => {});\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/sandbox/fleet.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface SandboxCommandResult {
|
|
|
29
29
|
*/
|
|
30
30
|
export interface MaterializationSandbox {
|
|
31
31
|
readonly id: string;
|
|
32
|
+
/** Human-readable provider name, forwarded from the underlying sandbox. */
|
|
33
|
+
readonly name?: string;
|
|
34
|
+
/** Provider type discriminator, forwarded from the underlying sandbox. */
|
|
35
|
+
readonly provider?: string;
|
|
36
|
+
/** Sandbox usage instructions surfaced in tool descriptions. */
|
|
37
|
+
getInstructions?(opts?: {
|
|
38
|
+
requestContext?: unknown;
|
|
39
|
+
}): string;
|
|
40
|
+
/** Long-running process capability, when the provider supports it. */
|
|
41
|
+
readonly processes?: WorkspaceSandbox['processes'];
|
|
42
|
+
/** Mount capability, when the provider supports it. */
|
|
43
|
+
readonly mounts?: WorkspaceSandbox['mounts'];
|
|
32
44
|
start(): Promise<void>;
|
|
33
45
|
getInfo(): Promise<{
|
|
34
46
|
metadata?: Record<string, unknown>;
|
|
@@ -41,6 +53,12 @@ export interface MaterializationSandbox {
|
|
|
41
53
|
setEnvironmentVariable?(name: string, value: string): void;
|
|
42
54
|
/** Tear down the underlying VM. Optional: providers without it are no-ops. */
|
|
43
55
|
stop?(): Promise<void>;
|
|
56
|
+
/** True when the provider persists real checkpoints (snapshot is not a no-op). */
|
|
57
|
+
readonly supportsCheckpoints?: boolean;
|
|
58
|
+
/** Persist the sandbox's current state under its bound checkpoint name. */
|
|
59
|
+
snapshot?(): Promise<void>;
|
|
60
|
+
/** Boot-only fallback checkpoint requested for this fresh provision. */
|
|
61
|
+
seedCheckpointNameUsed?: string;
|
|
44
62
|
}
|
|
45
63
|
/** Options for building (or reattaching) one sandbox. */
|
|
46
64
|
export interface SandboxCreateOptions {
|
|
@@ -58,6 +76,12 @@ export interface SandboxCreateOptions {
|
|
|
58
76
|
idleTimeoutMinutes?: number;
|
|
59
77
|
/** Provider checkpoint used to seed and preserve this sandbox's filesystem. */
|
|
60
78
|
checkpointName?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Boot-only fallback checkpoint used when `checkpointName` has no stored
|
|
81
|
+
* state yet (e.g. the repo base checkpoint for a brand-new session).
|
|
82
|
+
* Snapshots keep writing to `checkpointName`.
|
|
83
|
+
*/
|
|
84
|
+
seedCheckpointName?: string;
|
|
61
85
|
/** Opaque user subject attributed to provider API requests. */
|
|
62
86
|
actingUserId?: string;
|
|
63
87
|
}
|
|
@@ -104,6 +128,8 @@ export interface SandboxBindingStore {
|
|
|
104
128
|
readonly sandboxId: string | null;
|
|
105
129
|
/** Provider checkpoint used to seed and preserve this sandbox's filesystem. */
|
|
106
130
|
readonly checkpointName?: string;
|
|
131
|
+
/** Boot-only fallback checkpoint (e.g. repo base checkpoint) for first provision. */
|
|
132
|
+
readonly seedCheckpointName?: string;
|
|
107
133
|
/** Persist a freshly provisioned provider id, or clear a stale one with `null`. */
|
|
108
134
|
setSandboxId(id: string | null): Promise<void>;
|
|
109
135
|
/** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */
|
|
@@ -150,6 +176,12 @@ export declare class SandboxFleet {
|
|
|
150
176
|
* through {@link enabled}.
|
|
151
177
|
*/
|
|
152
178
|
get provider(): string;
|
|
179
|
+
/**
|
|
180
|
+
* Usage instructions from the configured template machine, for surfacing in
|
|
181
|
+
* tool descriptions before any per-session sandbox has materialized.
|
|
182
|
+
* Empty string when no machine is configured or it exposes none.
|
|
183
|
+
*/
|
|
184
|
+
getInstructions(): string;
|
|
153
185
|
/**
|
|
154
186
|
* Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.
|
|
155
187
|
* Read back from the machine's own config when it exposes one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI/D,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC3D,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;KAAE,GACvE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,0EAA0E;IAC1E,sBAAsB,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,8EAA8E;IAC9E,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
|
package/dist/sandbox/fleet.js
CHANGED
|
@@ -61,6 +61,11 @@ function toMaterializationSandbox(sandbox, initialEnvironment = {}) {
|
|
|
61
61
|
const environment = { ...initialEnvironment };
|
|
62
62
|
return {
|
|
63
63
|
id: sandbox.id,
|
|
64
|
+
name: sandbox.name,
|
|
65
|
+
provider: sandbox.provider,
|
|
66
|
+
getInstructions: (opts) => sandbox.getInstructions?.(opts) ?? "",
|
|
67
|
+
processes: sandbox.processes,
|
|
68
|
+
mounts: sandbox.mounts,
|
|
64
69
|
start: async () => {
|
|
65
70
|
await (lifecycle._start ?? sandbox.start)?.call(sandbox);
|
|
66
71
|
},
|
|
@@ -77,19 +82,23 @@ function toMaterializationSandbox(sandbox, initialEnvironment = {}) {
|
|
|
77
82
|
},
|
|
78
83
|
stop: async () => {
|
|
79
84
|
await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);
|
|
80
|
-
}
|
|
85
|
+
},
|
|
86
|
+
supportsCheckpoints: sandbox.supportsCheckpoints === true,
|
|
87
|
+
...sandbox.snapshot ? { snapshot: sandbox.snapshot.bind(sandbox) } : {}
|
|
81
88
|
};
|
|
82
89
|
}
|
|
83
90
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* provider-native id (e.g. local) reattach by construction id, so fall back
|
|
87
|
-
* to the sandbox's own logical id.
|
|
91
|
+
* Provider details reported after startup. Railway exposes its native sandbox
|
|
92
|
+
* id and the checkpoint it actually restored through `getInfo().metadata`.
|
|
88
93
|
*/
|
|
89
|
-
async function
|
|
94
|
+
async function readProviderSandboxDetails(sandbox) {
|
|
90
95
|
const info = await sandbox.getInfo();
|
|
91
96
|
const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;
|
|
92
|
-
|
|
97
|
+
const restoredCheckpointName = info.metadata?.restoredCheckpointName;
|
|
98
|
+
return {
|
|
99
|
+
sandboxId: typeof id === "string" ? id : sandbox.id,
|
|
100
|
+
restoredCheckpointName: typeof restoredCheckpointName === "string" ? restoredCheckpointName : void 0
|
|
101
|
+
};
|
|
93
102
|
}
|
|
94
103
|
/** Keep each path piece a single safe segment (no separators or traversal). */
|
|
95
104
|
function sanitizeSegment(segment) {
|
|
@@ -135,6 +144,14 @@ var SandboxFleet = class {
|
|
|
135
144
|
return this.#config?.machine.provider ?? "none";
|
|
136
145
|
}
|
|
137
146
|
/**
|
|
147
|
+
* Usage instructions from the configured template machine, for surfacing in
|
|
148
|
+
* tool descriptions before any per-session sandbox has materialized.
|
|
149
|
+
* Empty string when no machine is configured or it exposes none.
|
|
150
|
+
*/
|
|
151
|
+
getInstructions() {
|
|
152
|
+
return this.#config?.machine.getInstructions?.() ?? "";
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
138
155
|
* Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.
|
|
139
156
|
* Read back from the machine's own config when it exposes one
|
|
140
157
|
* (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the
|
|
@@ -227,6 +244,7 @@ var SandboxFleet = class {
|
|
|
227
244
|
...opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {},
|
|
228
245
|
...opts.idleTimeoutMinutes !== void 0 ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {},
|
|
229
246
|
...opts.checkpointName ? { checkpointName: opts.checkpointName } : {},
|
|
247
|
+
...opts.seedCheckpointName ? { seedCheckpointName: opts.seedCheckpointName } : {},
|
|
230
248
|
...opts.actingUserId ? { actingUserId: opts.actingUserId } : {}
|
|
231
249
|
}), opts.env);
|
|
232
250
|
}
|
|
@@ -263,6 +281,7 @@ var SandboxFleet = class {
|
|
|
263
281
|
});
|
|
264
282
|
try {
|
|
265
283
|
await timedPhase("sandbox.reattach", () => reattached.start());
|
|
284
|
+
reattached.seedCheckpointNameUsed = void 0;
|
|
266
285
|
return reattached;
|
|
267
286
|
} catch {
|
|
268
287
|
await store.setSandboxId(null);
|
|
@@ -277,15 +296,25 @@ var SandboxFleet = class {
|
|
|
277
296
|
const sandbox = this.#build({
|
|
278
297
|
idleTimeoutMinutes,
|
|
279
298
|
...checkpointName ? { checkpointName } : {},
|
|
299
|
+
...store.seedCheckpointName ? { seedCheckpointName: store.seedCheckpointName } : {},
|
|
280
300
|
...env ? { env } : {},
|
|
281
301
|
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {},
|
|
282
302
|
...options.actingUserId ? { actingUserId: options.actingUserId } : {}
|
|
283
303
|
});
|
|
284
304
|
await timedPhase("sandbox.provision", () => sandbox.start());
|
|
285
305
|
this.#liveCount += 1;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
try {
|
|
307
|
+
const provider = await readProviderSandboxDetails(sandbox);
|
|
308
|
+
if (provider.sandboxId) await store.setSandboxId(provider.sandboxId);
|
|
309
|
+
if (store.seedCheckpointName && provider.restoredCheckpointName === store.seedCheckpointName) sandbox.seedCheckpointNameUsed = store.seedCheckpointName;
|
|
310
|
+
return sandbox;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (this.#liveCount > 0) this.#liveCount -= 1;
|
|
313
|
+
try {
|
|
314
|
+
await sandbox.stop?.();
|
|
315
|
+
} catch {}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
289
318
|
}
|
|
290
319
|
/**
|
|
291
320
|
* Tear down a sandbox binding: stop the live VM (best-effort) and clear the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { timedPhase } from '../timing.js';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n /** Opaque user subject attributed to provider API requests. */\n actingUserId?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Opaque user subject attributed to provider API requests. */\n actingUserId?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n ...(opts.actingUserId ? { actingUserId: opts.actingUserId } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n try {\n await timedPhase('sandbox.reattach', () => reattached.start());\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n await timedPhase('sandbox.provision', () => sandbox.start());\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsFA,SAAgB,eAAe,YAAoC,OAA8B;CAC/F,IAAI,CAAC,YAAY;CACjB,IAAI;EACF,WAAW,KAAK;CAClB,QAAQ,CAER;AACF;;AAUA,IAAa,qBAAb,cAAwC,MAAM;CAEvB;CADrB,OAAgB;CAChB,YAAY,KAAsB;EAChC,MACE,oDAAoD,IAAI,+GAE1D;EAJmB,KAAA,MAAA;EAKnB,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAmCA,SAAS,YAAY,OAAgD;CACnE,IAAI,MAAM,gBAAgB,OAAO,cAAc,MAAM;CACrD,IAAI,MAAM,WAAW,OAAO,WAAW,MAAM;AAE/C;;;;;;;AAQA,SAAS,yBACP,SACA,qBAA6C,CAAC,GACtB;CACxB,IAAI,OAAO,QAAQ,mBAAmB,YACpC,MAAM,IAAI,MACR,qBAAqB,QAAQ,SAAS,kEACxC;CAEF,MAAM,YAAY;CAClB,MAAM,cAAc,EAAE,GAAG,mBAAmB;CAC5C,OAAO;EACL,IAAI,QAAQ;EACZ,OAAO,YAAY;GACjB,OAAO,UAAU,UAAU,QAAQ,MAAA,EAAQ,KAAK,OAAO;EACzD;EACA,SAAS,YAAa,MAAM,QAAQ,UAAU,KAAM,CAAC;EACrD,iBAAiB,SAAS,MAAM,YAC9B,QAAQ,eAAgB,SAAS,MAAM;GACrC,GAAG;GACH,KAAK;IAAE,GAAG;IAAa,GAAG,SAAS;GAAI;EACzC,CAAC;EACH,yBAAyB,MAAM,UAAU;GACvC,YAAY,QAAQ;EACtB;EACA,MAAM,YAAY;GAChB,OAAO,UAAU,SAAS,QAAQ,KAAA,EAAO,KAAK,OAAO;EACvD;CACF;AACF;;;;;;;AAQA,eAAe,sBAAsB,SAA8D;CACjG,MAAM,OAAO,MAAM,QAAQ,QAAQ;CACnC,MAAM,KAAK,KAAK,UAAU,oBAAoB,KAAK,UAAU;CAC7D,OAAO,OAAO,OAAO,WAAW,KAAK,QAAQ;AAC/C;;AAGA,SAAS,gBAAgB,SAAyB;CAEhD,OADgB,QAAQ,QAAQ,oBAAoB,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAC5D,KAAK;AACpB;;AAGA,SAAgB,6BAA6B,MAAc,GAAG,UAA4B;CACxF,MAAM,eAAe,KAAK,QAAQ,IAAI;CACtC,MAAM,WAAW,KAAK,QAAQ,cAAc,GAAG,QAAQ;CACvD,IAAI,aAAa,gBAAgB,SAAS,WAAW,GAAG,eAAe,KAAK,KAAK,GAAG,OAAO;CAC3F,MAAM,IAAI,MAAM,+DAA+D,UAAU;AAC3F;;;;;;;AA0BA,IAAa,eAAb,MAA0B;CACxB;CACA;CACA,aAAa;;CAEb,4BAAqB,IAAI,IAA6C;CAEtE,YAAY,QAA6B;EACvC,KAAKA,UAAU;CACjB;;;;;;CAOA,IAAI,UAAmB;EACrB,OAAO,KAAKA,YAAY,KAAA;CAC1B;;;;;;;CAQA,IAAI,WAAmB;EACrB,OAAO,KAAKA,SAAS,QAAQ,YAAY;CAC3C;;;;;;;;;CAUA,IAAI,cAAsB;EAExB,MAAM,WADU,KAAKA,SAAS,QAAA,EACL;EACzB,OAAO,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU;CAC5F;;;;;;;CAQA,IAAI,eAAuB;EACzB,OAAO,KAAKA,SAAS,gBAAgB;CACvC;;;;;;CAOA,IAAI,YAAoB;EACtB,OAAO,KAAKE;CACd;;CAGA,iBAAiB,QAAQ,GAAS;EAChC,KAAKA,aAAa;CACpB;;CAGA,WAAW,SAA+B;EACxC,KAAKC,WAAW;CAClB;;CAGA,eAAqB;EACnB,KAAKA,WAAW,KAAA;CAClB;;;;;;;;;CAUA,eAAe,cAA8B;EAC3C,IAAI,CAAC,KAAKH,SAAS,MAAM,IAAI,MAAM,uBAAuB;EAC1D,MAAM,CAAC,OAAO,QAAQ,aAAa,MAAM,KAAK,CAAC;EAC/C,OAAO,GAAG,KAAKA,QAAQ,YAAY,GAAG,gBAAgB,SAAS,SAAS,EAAE,GAAG,gBAAgB,QAAQ,MAAM;CAC7G;;;;;;CAOA,2BAA2B,cAAsB,WAA2B;EAC1E,IAAI,CAAC,KAAKA,SAAS,MAAM,IAAI,MAAM,uBAAuB;EAC1D,IAAI,KAAKA,QAAQ,QAAQ,aAAa,SACpC,MAAM,IAAI,MAAM,2DAA2D;EAG7E,MAAM,YAAa,KAAKA,QAAQ,QAA2C;EAC3E,IAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GACxD,MAAM,IAAI,MAAM,mDAAmD;EAGrE,MAAM,CAAC,OAAO,QAAQ,aAAa,MAAM,KAAK,CAAC;EAC/C,OAAO,6BACL,WACA,mBACA,gBAAgB,SAAS,SAAS,GAClC,gBAAgB,QAAQ,MAAM,GAC9B,gBAAgB,SAAS,CAC3B;CACF;;;;;;;;;;;;;;;;CAiBA,OAAO,MAAoD;EACzD,IAAI,KAAKG,UAAU,OAAO,KAAKA,SAAS,IAAI;EAC5C,IAAI,CAAC,KAAKH,SAAS,MAAM,IAAI,MAAM,uBAAuB;EAQ1D,OAAO,yBAPO,KAAKA,QAAQ,QAAQ,MAAO;GACxC,GAAI,KAAK,oBAAoB;IAAE,IAAI,KAAK;IAAmB,WAAW,KAAK;GAAkB,IAAI,CAAC;GAClG,GAAI,KAAK,mBAAmB,EAAE,kBAAkB,KAAK,iBAAiB,IAAI,CAAC;GAC3E,GAAI,KAAK,uBAAuB,KAAA,IAAY,EAAE,oBAAoB,KAAK,mBAAmB,IAAI,CAAC;GAC/F,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;GACrE,GAAI,KAAK,eAAe,EAAE,cAAc,KAAK,aAAa,IAAI,CAAC;EACjE,CACoC,GAAG,KAAK,GAAG;CACjD;CAoBA,MAAM,cACJ,OACA,eACA,mBACA,eAAqC,CAAC,GACL;EACjC,MAAM,MAAM,OAAO,kBAAkB,aAAa,KAAA,IAAY;EAC9D,MAAM,aACJ,OAAO,kBAAkB,aAAa,gBAAiB;EACzD,MAAM,UACJ,OAAO,kBAAkB,aACnB,qBAA0D,CAAC,IAC7D;EAEN,MAAM,MAAM,YAAY,KAAK;EAC7B,IAAI,CAAC,KAAK,OAAO,KAAKI,0BAA0B,OAAO,KAAK,YAAY,OAAO;EAE/E,MAAM,WAAW,KAAKH,UAAU,IAAI,GAAG;EACvC,IAAI,UAAU,OAAO;EAErB,MAAM,UAAU,KAAKG,0BAA0B,OAAO,KAAK,YAAY,OAAO,CAAC,CAAC,cAAc;GAE5F,IAAI,KAAKH,UAAU,IAAI,GAAG,MAAM,SAAS,KAAKA,UAAU,OAAO,GAAG;EACpE,CAAC;EACD,KAAKA,UAAU,IAAI,KAAK,OAAO;EAC/B,OAAO;CACT;;CAGA,MAAMG,0BACJ,OACA,KACA,YACA,SACiC;EACjC,MAAM,qBAAqB,KAAK;EAChC,MAAM,iBAAiB,MAAM;EAM7B,IAAI,MAAM,WAAW;GACnB,eAAe,YAAY;IAAE,OAAO;IAAe,SAAS;GAAgC,CAAC;GAC7F,MAAM,aAAa,KAAKC,OAAO;IAC7B,mBAAmB,MAAM;IACzB;IACA,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;IAC3C,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,QAAQ,mBAAmB,EAAE,kBAAkB,QAAQ,iBAAiB,IAAI,CAAC;IACjF,GAAI,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;GACvE,CAAC;GACD,IAAI;IACF,MAAM,WAAW,0BAA0B,WAAW,MAAM,CAAC;IAC7D,OAAO;GACT,QAAQ;IACN,MAAM,MAAM,aAAa,IAAI;GAE/B;EACF;EAGA,MAAM,MAAM,KAAK;EACjB,IAAI,MAAM,KAAK,KAAKH,cAAc,KAChC,MAAM,IAAI,mBAAmB,GAAG;EAGlC,eAAe,YAAY;GAAE,OAAO;GAAgB,SAAS;EAA8B,CAAC;EAC5F,MAAM,UAAU,KAAKG,OAAO;GAC1B;GACA,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;GAC3C,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;GACrB,GAAI,QAAQ,mBAAmB,EAAE,kBAAkB,QAAQ,iBAAiB,IAAI,CAAC;GACjF,GAAI,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;EACvE,CAAC;EACD,MAAM,WAAW,2BAA2B,QAAQ,MAAM,CAAC;EAC3D,KAAKH,cAAc;EAEnB,MAAM,oBAAoB,MAAM,sBAAsB,OAAO;EAC7D,IAAI,mBACF,MAAM,MAAM,aAAa,iBAAiB;EAG5C,OAAO;CACT;;;;;;;;;CAUA,MAAM,gBAAgB,OAA4B,SAAiD;EACjG,IAAI,SAAS,MACX,IAAI;GACF,MAAM,QAAQ,KAAK;EACrB,QAAQ,CAER;EAEF,IAAI,MAAM,WAAW;GACnB,IAAI,KAAKA,aAAa,GAAG,KAAKA,cAAc;GAC5C,MAAM,MAAM,MAAM;EACpB;CACF;;;;;;;CAQA,MAAM,gBACJ,mBACA,UAAgC,CAAC,GACA;EACjC,MAAM,UAAU,KAAKG,OAAO;GAC1B;GACA,oBAAoB,KAAK;GACzB,GAAI,QAAQ,mBAAmB,EAAE,kBAAkB,QAAQ,iBAAiB,IAAI,CAAC;GACjF,GAAI,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;EACvE,CAAC;EACD,MAAM,QAAQ,MAAM;EACpB,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { timedPhase } from '../timing.js';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n /** Human-readable provider name, forwarded from the underlying sandbox. */\n readonly name?: string;\n /** Provider type discriminator, forwarded from the underlying sandbox. */\n readonly provider?: string;\n /** Sandbox usage instructions surfaced in tool descriptions. */\n getInstructions?(opts?: { requestContext?: unknown }): string;\n /** Long-running process capability, when the provider supports it. */\n readonly processes?: WorkspaceSandbox['processes'];\n /** Mount capability, when the provider supports it. */\n readonly mounts?: WorkspaceSandbox['mounts'];\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n /** True when the provider persists real checkpoints (snapshot is not a no-op). */\n readonly supportsCheckpoints?: boolean;\n /** Persist the sandbox's current state under its bound checkpoint name. */\n snapshot?(): Promise<void>;\n /** Boot-only fallback checkpoint requested for this fresh provision. */\n seedCheckpointNameUsed?: string;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n /**\n * Boot-only fallback checkpoint used when `checkpointName` has no stored\n * state yet (e.g. the repo base checkpoint for a brand-new session).\n * Snapshots keep writing to `checkpointName`.\n */\n seedCheckpointName?: string;\n /** Opaque user subject attributed to provider API requests. */\n actingUserId?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Opaque user subject attributed to provider API requests. */\n actingUserId?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Boot-only fallback checkpoint (e.g. repo base checkpoint) for first provision. */\n readonly seedCheckpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n name: sandbox.name,\n provider: sandbox.provider,\n getInstructions: opts =>\n sandbox.getInstructions?.(opts as Parameters<NonNullable<WorkspaceSandbox['getInstructions']>>[0]) ?? '',\n processes: sandbox.processes,\n mounts: sandbox.mounts,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n supportsCheckpoints: sandbox.supportsCheckpoints === true,\n ...(sandbox.snapshot ? { snapshot: sandbox.snapshot.bind(sandbox) } : {}),\n };\n}\n\n/**\n * Provider details reported after startup. Railway exposes its native sandbox\n * id and the checkpoint it actually restored through `getInfo().metadata`.\n */\nasync function readProviderSandboxDetails(\n sandbox: MaterializationSandbox,\n): Promise<{ sandboxId: string | undefined; restoredCheckpointName: string | undefined }> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n const restoredCheckpointName = info.metadata?.restoredCheckpointName;\n return {\n sandboxId: typeof id === 'string' ? id : sandbox.id,\n restoredCheckpointName: typeof restoredCheckpointName === 'string' ? restoredCheckpointName : undefined,\n };\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Usage instructions from the configured template machine, for surfacing in\n * tool descriptions before any per-session sandbox has materialized.\n * Empty string when no machine is configured or it exposes none.\n */\n getInstructions(): string {\n return this.#config?.machine.getInstructions?.() ?? '';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n ...(opts.seedCheckpointName ? { seedCheckpointName: opts.seedCheckpointName } : {}),\n ...(opts.actingUserId ? { actingUserId: opts.actingUserId } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n try {\n await timedPhase('sandbox.reattach', () => reattached.start());\n reattached.seedCheckpointNameUsed = undefined;\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n // Boot-only fallback seed (repo base checkpoint) — only meaningful on a\n // fresh provision; snapshots keep writing to `checkpointName`.\n ...(store.seedCheckpointName ? { seedCheckpointName: store.seedCheckpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n await timedPhase('sandbox.provision', () => sandbox.start());\n this.#liveCount += 1;\n\n try {\n const provider = await readProviderSandboxDetails(sandbox);\n if (provider.sandboxId) {\n await store.setSandboxId(provider.sandboxId);\n }\n\n if (store.seedCheckpointName && provider.restoredCheckpointName === store.seedCheckpointName) {\n sandbox.seedCheckpointNameUsed = store.seedCheckpointName;\n }\n return sandbox;\n } catch (error) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n try {\n await sandbox.stop?.();\n } catch {\n // Preserve the provider-detail or persistence error that made the sandbox unusable.\n }\n throw error;\n }\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n ...(options.actingUserId ? { actingUserId: options.actingUserId } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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"}
|
|
@@ -10,8 +10,9 @@ export interface CheckpointCaptureSession {
|
|
|
10
10
|
* Snapshot the session's sandbox before every agent-end event so providers
|
|
11
11
|
* with checkpoint support (for example Railway-backed sandboxes) persist the
|
|
12
12
|
* last completed turn's writes. Captures are chained sequentially so a slow
|
|
13
|
-
* snapshot never overlaps the next one,
|
|
14
|
-
*
|
|
13
|
+
* snapshot never overlaps the next one, but the chain is intentionally NOT
|
|
14
|
+
* returned to the session: turn completion must never block on snapshot I/O.
|
|
15
|
+
* Failures are logged rather than thrown so they never break the agent turn.
|
|
15
16
|
*/
|
|
16
17
|
export declare function observeSessionCheckpoint(session: CheckpointCaptureSession): () => void;
|
|
17
18
|
//# sourceMappingURL=checkpoint-capture.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-capture.d.ts","sourceRoot":"","sources":["../../src/session/checkpoint-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,wBAAwB;IACvC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;CACvE;AAED
|
|
1
|
+
{"version":3,"file":"checkpoint-capture.d.ts","sourceRoot":"","sources":["../../src/session/checkpoint-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,wBAAwB;IACvC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;CACvE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAgBtF"}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Snapshot the session's sandbox before every agent-end event so providers
|
|
4
4
|
* with checkpoint support (for example Railway-backed sandboxes) persist the
|
|
5
5
|
* last completed turn's writes. Captures are chained sequentially so a slow
|
|
6
|
-
* snapshot never overlaps the next one,
|
|
7
|
-
*
|
|
6
|
+
* snapshot never overlaps the next one, but the chain is intentionally NOT
|
|
7
|
+
* returned to the session: turn completion must never block on snapshot I/O.
|
|
8
|
+
* Failures are logged rather than thrown so they never break the agent turn.
|
|
8
9
|
*/
|
|
9
10
|
function observeSessionCheckpoint(session) {
|
|
10
11
|
let capture = Promise.resolve();
|
|
@@ -18,7 +19,6 @@ function observeSessionCheckpoint(session) {
|
|
|
18
19
|
console.warn("[Factory checkpoint capture] Unable to snapshot sandbox.", error);
|
|
19
20
|
}
|
|
20
21
|
});
|
|
21
|
-
return capture;
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-capture.js","names":[],"sources":["../../src/session/checkpoint-capture.ts"],"sourcesContent":["import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nexport interface CheckpointCaptureSession {\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'snapshot'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n}\n\n/**\n * Snapshot the session's sandbox before every agent-end event so providers\n * with checkpoint support (for example Railway-backed sandboxes) persist the\n * last completed turn's writes. Captures are chained sequentially so a slow\n * snapshot never overlaps the next one,
|
|
1
|
+
{"version":3,"file":"checkpoint-capture.js","names":[],"sources":["../../src/session/checkpoint-capture.ts"],"sourcesContent":["import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nexport interface CheckpointCaptureSession {\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'snapshot'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n}\n\n/**\n * Snapshot the session's sandbox before every agent-end event so providers\n * with checkpoint support (for example Railway-backed sandboxes) persist the\n * last completed turn's writes. Captures are chained sequentially so a slow\n * snapshot never overlaps the next one, but the chain is intentionally NOT\n * returned to the session: turn completion must never block on snapshot I/O.\n * Failures are logged rather than thrown so they never break the agent turn.\n */\nexport function observeSessionCheckpoint(session: CheckpointCaptureSession): () => void {\n let capture = Promise.resolve();\n return session.onBeforeAgentEnd(() => {\n capture = capture.then(async () => {\n // Chat-only sessions run without a workspace; there is nothing to snapshot.\n const sandbox = session.getWorkspace()?.sandbox;\n // Older sandbox implementations predate `snapshot()`; skip them quietly.\n if (typeof sandbox?.snapshot !== 'function') return;\n try {\n await sandbox.snapshot();\n } catch (error) {\n console.warn('[Factory checkpoint capture] Unable to snapshot sandbox.', error);\n }\n });\n // Fire-and-forget: do not gate the terminal agent event on the snapshot.\n });\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAgB,yBAAyB,SAA+C;CACtF,IAAI,UAAU,QAAQ,QAAQ;CAC9B,OAAO,QAAQ,uBAAuB;EACpC,UAAU,QAAQ,KAAK,YAAY;GAEjC,MAAM,UAAU,QAAQ,aAAa,CAAC,EAAE;GAExC,IAAI,OAAO,SAAS,aAAa,YAAY;GAC7C,IAAI;IACF,MAAM,QAAQ,SAAS;GACzB,SAAS,OAAO;IACd,QAAQ,KAAK,4DAA4D,KAAK;GAChF;EACF,CAAC;CAEH,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAuCzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CAAC,IAAI,EAAE;IAC1D,aAAa,EAAE,0BAA0B,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAc9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,2BAA2B,CAAC,
|
|
1
|
+
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAuCzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CAAC,IAAI,EAAE;IAC1D,aAAa,EAAE,0BAA0B,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAc9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,2BAA2B,CAAC,CA6BtC;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnH"}
|