fix: handle restore rollback and spawn id collisions

This commit is contained in:
harshitsinghbhandari 2026-05-28 17:25:02 +05:30
parent e66988ab77
commit fcb3aec988
2 changed files with 39 additions and 0 deletions

View File

@ -101,6 +101,11 @@ func New(d Deps) *Manager {
// step eagerly rolls back the steps that already succeeded.
func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Session, error) {
id := m.newID(cfg)
if _, ok, err := m.store.Get(ctx, id); err != nil {
return domain.Session{}, fmt.Errorf("spawn %s: check existing: %w", id, err)
} else if ok {
return domain.Session{}, fmt.Errorf("spawn %s: already exists", id)
}
ws, err := m.workspace.Create(ctx, ports.WorkspaceConfig{
ProjectID: cfg.ProjectID,
@ -315,6 +320,9 @@ func (m *Manager) Restore(ctx context.Context, id domain.SessionID) (domain.Sess
}
if err := m.lcm.OnSpawnCompleted(ctx, id, outcome); err != nil {
m.rollbackRuntime(ctx, handle)
if revertErr := m.lcm.OnSpawnInitiated(ctx, rec); revertErr != nil {
return domain.Session{}, fmt.Errorf("restore %s: revert after spawn completed failure: %w (original error: %v)", id, revertErr, err)
}
return domain.Session{}, fmt.Errorf("restore %s: on spawn completed: %w", id, err)
}
return m.Get(ctx, id)

View File

@ -121,6 +121,32 @@ func TestSpawn_RuntimeCreateFailure_RollsBack(t *testing.T) {
}
}
func TestSpawn_ExistingSessionIDRejectedBeforeWork(t *testing.T) {
h := newHarness("sess-1")
ctx := context.Background()
if err := h.store.Upsert(ctx, domain.SessionRecord{
ID: "sess-1",
ProjectID: testProject,
Lifecycle: lc(domain.SessionWorking, domain.ReasonTaskInProgress, domain.PRNone, ""),
}); err != nil {
t.Fatalf("seed existing row: %v", err)
}
_, err := h.sm.Spawn(ctx, spawnCfg())
if err == nil {
t.Fatal("spawn: want error for existing session id, got nil")
}
if len(h.workspace.created) != 0 {
t.Error("workspace should not be created when session id already exists")
}
if len(h.runtime.created) != 0 {
t.Error("runtime should not be created when session id already exists")
}
if h.log.indexOf("OnSpawnInitiated") != -1 || h.log.indexOf("OnSpawnCompleted") != -1 {
t.Error("LCM should not be called when session id already exists")
}
}
func TestSpawn_OnSpawnCompletedFailure_RoutesOrphanToErrored(t *testing.T) {
h := newHarness("sess-1")
ctx := context.Background()
@ -457,6 +483,11 @@ func TestRestore_OnSpawnCompletedFailure_RollsBackRuntime(t *testing.T) {
t.Fatal("restore: want error, got nil")
}
rec, _, _ := h.store.Get(ctx, "sess-1")
if got := rec.Lifecycle.Session; got.State != domain.SessionTerminated || got.Reason != domain.ReasonManuallyKilled {
t.Fatalf("restore failure should restore terminal lifecycle, got %+v", got)
}
// The runtime created during restore is torn back down so no process is
// stranded; the workspace is left intact (it holds the agent's prior work).
if len(h.runtime.destroyed) != destroyedBefore+1 {