Skip to content

Commit 018d672

Browse files
committed
fix supervisor rollback and busy state
1 parent c84dee5 commit 018d672

5 files changed

Lines changed: 87 additions & 9 deletions

File tree

.changeset/sharp-bottles-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@spencer-kit/coder-studio": patch
3+
---
4+
5+
Fix supervisor creation rollback when target files fail, and keep manual trigger disabled while an earlier supervisor cycle is still in flight.

packages/server/src/__tests__/supervisor-manager.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,26 @@ describe("SupervisorManager cycle triggers", () => {
373373
expect(managerInternals.evaluator.logger).toBe(deps.logger);
374374
});
375375

376+
it("deletes the persisted supervisor when create target files fails", async () => {
377+
const createTargetFilesError = new Error("disk full");
378+
deps.targetStore.createTargetFiles.mockImplementationOnce(async () => {
379+
throw createTargetFilesError;
380+
});
381+
382+
await expect(
383+
manager.create({
384+
sessionId: "sess-create-fails",
385+
workspaceId: "ws-1",
386+
objective: "Ship the fix",
387+
evaluatorProviderId: "codex",
388+
})
389+
).rejects.toThrow("disk full");
390+
391+
expect(deps.supervisorRepo.delete).toHaveBeenCalledWith(expect.any(String));
392+
expect(manager.getBySession("sess-create-fails")).toBeUndefined();
393+
expect(deps.supervisorRepo.getBySessionId("sess-create-fails")).toBeUndefined();
394+
});
395+
376396
it("returns an in-flight cycle immediately on manual triggerEvaluation", async () => {
377397
const supervisor = await manager.create({
378398
sessionId: "sess-manual",

packages/server/src/supervisor/manager.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -387,15 +387,21 @@ export class SupervisorManager {
387387
updatedAt: now,
388388
})
389389
);
390-
await this.deps.targetStore.createTargetFiles(workspace.path, {
391-
targetId,
392-
sessionId: req.sessionId,
393-
workspaceId: req.workspaceId,
394-
objective,
395-
createdAt: now,
396-
});
390+
let enriched: Supervisor;
391+
try {
392+
await this.deps.targetStore.createTargetFiles(workspace.path, {
393+
targetId,
394+
sessionId: req.sessionId,
395+
workspaceId: req.workspaceId,
396+
objective,
397+
createdAt: now,
398+
});
397399

398-
const enriched = await this.attachTargetState(supervisor, workspace.path);
400+
enriched = await this.attachTargetState(supervisor, workspace.path);
401+
} catch (error) {
402+
this.deps.supervisorRepo.delete(supervisor.id);
403+
throw error;
404+
}
399405

400406
this.storeSnapshot(enriched);
401407
this.broadcastState(enriched, "created");

packages/web/src/features/supervisor/actions/use-supervisor-actions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
102102
: ([] as SupervisorCycle[]);
103103

104104
const latestCycle = cycles[0];
105+
const hasInFlightCycle = cycles.some(
106+
(cycle) => cycle.status === "evaluating" || cycle.status === "queued"
107+
);
105108
const latestCycleText = latestCycle
106109
? (latestCycle.result ??
107110
latestCycle.errorReason ??
@@ -166,7 +169,8 @@ export function useSupervisorActions({ sessionId }: UseSupervisorActionsArgs) {
166169
handlePause,
167170
handleResume,
168171
handleTrigger,
169-
isBusy: supervisor?.state === "evaluating" || supervisor?.state === "injecting",
172+
isBusy:
173+
supervisor?.state === "evaluating" || supervisor?.state === "injecting" || hasInFlightCycle,
170174
latestCycle,
171175
latestCycleText,
172176
planGeneratedLabel,

packages/web/src/features/supervisor/components/supervisor-card.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,49 @@ describe("SupervisorCard", () => {
332332
expect(screen.getByRole("button", { name: "Pause" })).not.toBeDisabled();
333333
});
334334

335+
it("keeps manual trigger disabled while an older cycle is still evaluating", () => {
336+
const store = createStore();
337+
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
338+
store.set(localeAtom, "en");
339+
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
340+
store.set(
341+
supervisorsAtom,
342+
new Map([
343+
[
344+
"sess-1",
345+
{
346+
...createSupervisor(),
347+
state: "idle",
348+
objective: "Finish the follow-up refactor",
349+
},
350+
],
351+
])
352+
);
353+
store.set(
354+
supervisorCyclesAtom,
355+
new Map([
356+
[
357+
"sup-1",
358+
[
359+
createCycle({
360+
status: "evaluating",
361+
objective: "Finish the original refactor",
362+
completedAt: undefined,
363+
}),
364+
],
365+
],
366+
])
367+
);
368+
369+
render(
370+
<Provider store={store}>
371+
<SupervisorCard sessionId="sess-1" workspaceId="ws-1" />
372+
</Provider>
373+
);
374+
375+
expect(screen.getByRole("button", { name: "Trigger Evaluation" })).toBeDisabled();
376+
});
377+
335378
it("renders configured execution policy metadata", () => {
336379
const store = createStore();
337380
const scheduledAt = Date.UTC(2026, 4, 11, 3, 0);

0 commit comments

Comments
 (0)