feat: empty-container spawn (scratch workspace, no repo volume); fix bridge session isolation — explicit SessionManager per spawn stops SDK resuming the repo's old session (which also overrode LVMH_MODEL)
This commit is contained in:
+30
-21
@@ -239,7 +239,7 @@ func (s *Spawner) deleteJob(sessionID string) {
|
||||
}
|
||||
|
||||
// Start launches the async spawn pipeline and returns the new sessionId.
|
||||
func (s *Spawner) Start(ctx context.Context, repo, branch, model string) (SpawnResult, error) {
|
||||
func (s *Spawner) Start(ctx context.Context, repo, branch, model string, empty bool) (SpawnResult, error) {
|
||||
exists, err := s.imageExists(ctx)
|
||||
if err != nil {
|
||||
return SpawnResult{}, fmt.Errorf("docker unavailable: %w", err)
|
||||
@@ -253,7 +253,7 @@ func (s *Spawner) Start(ctx context.Context, repo, branch, model string) (SpawnR
|
||||
}
|
||||
sessionID := newUUID()
|
||||
s.setJob(sessionID, repo, stateCloning, "", "")
|
||||
go s.runJob(repo, branch, model, sessionID)
|
||||
go s.runJob(repo, branch, model, empty, sessionID)
|
||||
return SpawnResult{SessionID: sessionID, ImageUsed: s.resolveImage(repo)}, nil
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ func (s *Spawner) slugLock(slug string) *sync.Mutex {
|
||||
}
|
||||
|
||||
// runJob is the async clone→build→create→start pipeline.
|
||||
func (s *Spawner) runJob(repo, branch, model, sessionID string) {
|
||||
func (s *Spawner) runJob(repo, branch, model string, empty bool, sessionID string) {
|
||||
slug := repoSlug(repo)
|
||||
lock := s.slugLock(slug)
|
||||
lock.Lock()
|
||||
@@ -308,7 +308,7 @@ func (s *Spawner) runJob(repo, branch, model, sessionID string) {
|
||||
return
|
||||
}
|
||||
s.setJob(sessionID, repo, stateCreating, "", "")
|
||||
containerID, image, err := s.createAndStart(s.ctx, repo, slug, model, sessionID)
|
||||
containerID, image, err := s.createAndStart(s.ctx, repo, slug, model, empty, sessionID)
|
||||
if err != nil {
|
||||
s.setJob(sessionID, repo, stateError, "", err.Error())
|
||||
return
|
||||
@@ -522,7 +522,7 @@ func extractBuildError(body []byte) string {
|
||||
// createAndStart provisions volumes, creates and starts the worker container.
|
||||
// A repo-registered custom image (see /api/repos) overrides the default
|
||||
// worker image; the ops agent builds and registers those.
|
||||
func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model, sessionID string) (string, string, error) {
|
||||
func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model string, empty bool, sessionID string) (string, string, error) {
|
||||
image := s.resolveImage(repo)
|
||||
if image != imageRefWorker {
|
||||
exists, err := s.imageRefExists(ctx, image)
|
||||
@@ -533,19 +533,25 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model, session
|
||||
return "", "", fmt.Errorf("custom image %s not built (ask the ops agent to build it)", image)
|
||||
}
|
||||
}
|
||||
repoVolume := volumeRepoPrefix + slug
|
||||
fresh, err := s.ensureRepoVolume(ctx, repoVolume)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if fresh {
|
||||
if err := s.seedVolume(ctx, slug, repoVolume, sessionID); err != nil {
|
||||
// drop the half-seeded volume so the next spawn retries fresh
|
||||
// instead of silently booting into an empty workspace.
|
||||
if rmErr := s.cli.VolumeRemove(context.Background(), repoVolume, true); rmErr != nil {
|
||||
log.Printf("spawner: remove failed seed volume %s: %v", repoVolume, rmErr)
|
||||
var repoVolume string
|
||||
if empty {
|
||||
// Scratch spawn: no repo, no shared workspace — a blank slate.
|
||||
repoVolume = ""
|
||||
} else {
|
||||
repoVolume = volumeRepoPrefix + slug
|
||||
fresh, err := s.ensureRepoVolume(ctx, repoVolume)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if fresh {
|
||||
if err := s.seedVolume(ctx, slug, repoVolume, sessionID); err != nil {
|
||||
// drop the half-seeded volume so the next spawn retries fresh
|
||||
// instead of silently booting into an empty workspace.
|
||||
if rmErr := s.cli.VolumeRemove(context.Background(), repoVolume, true); rmErr != nil {
|
||||
log.Printf("spawner: remove failed seed volume %s: %v", repoVolume, rmErr)
|
||||
}
|
||||
return "", "", fmt.Errorf("seed %s: %w", repoVolume, err)
|
||||
}
|
||||
return "", "", fmt.Errorf("seed %s: %w", repoVolume, err)
|
||||
}
|
||||
}
|
||||
if _, err := s.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeSessions}); err != nil {
|
||||
@@ -555,11 +561,14 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model, session
|
||||
return "", "", fmt.Errorf("volume %s: %w", volumePiCache, err)
|
||||
}
|
||||
|
||||
binds := []string{
|
||||
repoVolume + ":" + workspaceMount,
|
||||
volumeSessions + ":" + sessionsMount,
|
||||
volumePiCache + ":" + cacheMount,
|
||||
var binds []string
|
||||
if !empty {
|
||||
binds = append(binds, repoVolume+":"+workspaceMount)
|
||||
}
|
||||
binds = append(binds,
|
||||
volumeSessions+":"+sessionsMount,
|
||||
volumePiCache+":"+cacheMount,
|
||||
)
|
||||
// Host pi credentials (OAuth tokens for anthropic etc.), read-only, so
|
||||
// spawned agents can use every model the catalog offers.
|
||||
if hostAgent := os.Getenv(envHostPiAgentDir); hostAgent != "" {
|
||||
|
||||
Reference in New Issue
Block a user