diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts index cca6ee08e7116..eae621ceb2ecf 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts @@ -1814,6 +1814,14 @@ describe("ThreadSettlementServiceV2 worker", () => { }); describe("ThreadSettlementServiceV2 terminals", () => { + const deletedEvent = (thread: OrchestrationV2AppThread): OrchestrationV2DomainEvent => ({ + type: "thread.deleted", + id: EventId.make(`event:deleted:${thread.id}`), + threadId: thread.id, + occurredAt: DateTime.makeUnsafe(NOW), + payload: { ...thread, deletedAt: DateTime.makeUnsafe(NOW) }, + }); + it.effect("keeps the terminals of a thread re-engaged before the event ran", () => Effect.scoped( Effect.gen(function* () { @@ -1925,10 +1933,11 @@ describe("ThreadSettlementServiceV2 terminals", () => { ), ); - it.effect("runs the settle script once when a settlement is re-emitted", () => + it.effect("runs the settle script once per settlement until the thread is deleted", () => Effect.scoped( Effect.gen(function* () { const repeated = appThread("repeated-thread", "settled", "/worktrees/repeated"); + const other = appThread("other-thread", "settled", "/worktrees/other"); const marker = appThread("marker-thread", "settled", "/worktrees/marker"); const fixture = yield* makeHarness({ snapshot: makeSnapshot([]), @@ -1941,8 +1950,14 @@ describe("ThreadSettlementServiceV2 terminals", () => { yield* startHarness(service, fixture.activation, fixture.snapshotReads); yield* fixture.publishEvent(settledEvent(repeated)); yield* fixture.publishEvent(settledEvent(repeated)); + yield* fixture.publishEvent(deletedEvent(other)); + yield* fixture.publishEvent(settledEvent(repeated)); + // A deleted thread cannot settle again; replaying its settlement shows the record is gone. + yield* fixture.publishEvent(deletedEvent(repeated)); + yield* fixture.publishEvent(settledEvent(repeated)); yield* fixture.publishEvent(settledEvent(marker)); assert.strictEqual((yield* Queue.take(fixture.scriptRuns)).threadId, repeated.id); + assert.strictEqual((yield* Queue.take(fixture.scriptRuns)).threadId, repeated.id); assert.strictEqual((yield* Queue.take(fixture.scriptRuns)).threadId, marker.id); }).pipe(Effect.provide(fixture.layer)); }), diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.ts index 05a13cd8a399f..7247a38329d74 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.ts @@ -678,8 +678,11 @@ export const make = Effect.gen(function* () { case "thread.settled": return cleanUpSettledThread(event.threadId).pipe(Effect.andThen(enqueueFullSweep())); case "thread.archived": - case "thread.deleted": return enqueueFullSweep(); + case "thread.deleted": + return Effect.sync(() => settleActionRunAt.delete(event.threadId)).pipe( + Effect.andThen(enqueueFullSweep()), + ); case "thread.pull-request-link-updated": case "thread.pull-request-synced": case "provider-session.detached":