Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -960,3 +960,60 @@ it.effect("wakes the parent once when its delegated task asks a question", () =>
});
}).pipe(Effect.provide(DelegatedTaskQuestionReactor.layer.pipe(Layer.provideMerge(layerTest)))),
);

it.effect("settling spends a snoozed thread's wake ticket, including after re-snoozing", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const projections = yield* ProjectionStore.ProjectionStoreV2;
const threadId = ThreadId.make("thread:settle-snoozed");
yield* orchestrator.dispatch({
type: "thread.create",
commandId: CommandId.make("create-settle-snoozed"),
threadId,
projectId: ProjectId.make("project:settle-snoozed"),
title: "Snoozed thread",
modelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
createdBy: "user",
creationSource: "web",
});
const now = yield* DateTime.now;
const snoozedUntil = DateTime.formatIso(DateTime.add(now, { hours: 1 }));
for (const round of [1, 2]) {
yield* orchestrator.dispatch({
type: "thread.snooze",
commandId: CommandId.make(`snooze-before-settle-${round}`),
threadId,
snoozedUntil,
});
const snoozed = yield* projections.getThreadShell(threadId);
assert.ok(snoozed?.snoozedUntil);
yield* orchestrator.dispatch({
type: "thread.settle",
commandId: CommandId.make(`settle-snoozed-${round}`),
threadId,
});
const settled = yield* projections.getThreadShell(threadId);
assert.ok(settled);
assert.equal(settled.settledOverride, "settled");
assert.equal(settled.snoozedUntil, null);
assert.equal(settled.snoozedAt, null);
const projection = yield* projections.getThreadProjection(threadId);
assert.equal(projection.thread.snoozedUntil, null);
assert.equal(projection.thread.snoozedAt, null);
}
yield* orchestrator.dispatch({
type: "thread.unsettle",
commandId: CommandId.make("unsettle-after-snooze"),
threadId,
reason: "user",
});
const active = yield* projections.getThreadShell(threadId);
assert.ok(active);
assert.equal(active.settledOverride, "active");
assert.equal(active.snoozedUntil, null);
}).pipe(Effect.provide(layerTest)),
);
11 changes: 8 additions & 3 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2870,8 +2870,8 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
case "thread.unarchive":
return { ...thread, archivedAt: null, updatedAt: now };
case "thread.settle": {
// Settling is "I'm done with this": it clears a pin the same way it
// parks the thread (mirrors the v1 decider's settle/pin exclusion).
// Settling is "I'm done with this": clear both the pin and snooze
// so the canonical shell stays on the settled shelf after confirmation.
const wasPinned = thread.pinnedAt != null;
const alreadySettled =
thread.settledOverride === "settled" && thread.settledAt !== null && !wasPinned;
Expand All @@ -2884,7 +2884,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
pinnedAt: null,
pinOrderKey: null,
activeOrderKey: null,
updatedAt: alreadySettled ? thread.updatedAt : now,
snoozedUntil: null,
snoozedAt: null,
updatedAt:
alreadySettled && thread.snoozedUntil == null && thread.snoozedAt == null
? thread.updatedAt
: now,
};
}
case "thread.unsettle": {
Expand Down
Loading