Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
effectiveSnoozed,
threadWokeAt,
isThreadWokeUnseen,
settledThreadLabel,
snoozeWakeLabel,
} from "@t3tools/client-runtime/state/thread-settled";
import type { WorktreeSetupCardProps } from "./worktree-setup-card";
Expand Down Expand Up @@ -843,7 +844,7 @@ function ThreadDetailBody(
lifecycleSnoozed && snoozedUntil !== null
? `Snoozed, wakes in ${snoozeWakeLabel(snoozedUntil, { now: lifecycleNow })}`
: settled
? "Settled"
? settledThreadLabel(props.selectedThread, null)
: "Woke from snooze";
const actionLabel = lifecycleSnoozed ? "Wake now" : settled ? "Un-settle" : "Dismiss";
return {
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/state/use-thread-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ function threadDetailToShell(
archivedAt: thread.archivedAt,
settledOverride: thread.settledOverride,
settledAt: thread.settledAt,
...(thread.settledOverride === "settled" && thread.settleReason
? { settleReason: thread.settleReason }
: {}),
unsettledAt: thread.unsettledAt,
activeOrderKey: thread.activeOrderKey,
autoSettleDisabledAt: thread.autoSettleDisabledAt,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3704,6 +3704,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
...thread,
settledOverride: "settled",
settledAt: alreadySettled ? thread.settledAt : (command.settledAt ?? now),
settleReason: alreadySettled
? (thread.settleReason ?? null)
: (command.reason ?? { type: "user" }),
pullRequests: thread.pullRequests?.map((link) => withPullRequestWatch(link, undefined)),
unsettledAt: null,
pinnedAt: null,
Expand Down Expand Up @@ -13399,6 +13402,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
commandId: command.commandId,
threadId: command.threadId,
...(command.settledAt === undefined ? {} : { settledAt: command.settledAt }),
...(command.reason === undefined ? {} : { reason: command.reason }),
},
events,
effects,
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/orchestration-v2/ProjectionSettlement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as ProjectionStore from "./ProjectionStore.ts";
import {
isAutoSettlementCandidate,
isSnoozed,
resolveAutoSettlementAt,
resolveAutoSettlement,
} from "./ThreadSettlementService.ts";

const layerSql = ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
Expand Down Expand Up @@ -344,8 +344,8 @@ it.effect(
autoSettleOnMerge: false,
};
assert.deepEqual(
resolveAutoSettlementAt({ ...settings, thread: candidate }),
resolveAutoSettlementAt({
resolveAutoSettlement({ ...settings, thread: candidate }),
resolveAutoSettlement({
...settings,
thread: {
...expected,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/orchestration-v2/ProjectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,15 @@ const linkCardStillAsks = (
latestUserAuthoredMessageAt === null ||
!DateTime.isGreaterThan(latestUserAuthoredMessageAt, item.startedAt ?? item.updatedAt);

// Unsettling leaves the reason on the thread record; shells send it only while it applies.
function settleReasonForShell(
thread: Pick<OrchestrationV2AppThread, "settledOverride" | "settleReason">,
) {
return thread.settledOverride === "settled" && thread.settleReason
? { settleReason: thread.settleReason }
: {};
}

export function threadShellFromProjection(
projection: OrchestrationV2ThreadProjection,
): OrchestrationV2ThreadShell {
Expand Down Expand Up @@ -1687,6 +1696,7 @@ export function threadShellFromProjection(
archivedAt: projection.thread.archivedAt,
settledOverride: projection.thread.settledOverride,
settledAt: projection.thread.settledAt,
...settleReasonForShell(projection.thread),
unsettledAt: projection.thread.unsettledAt ?? null,
snoozedUntil: projection.thread.snoozedUntil ?? null,
snoozedAt: projection.thread.snoozedAt ?? null,
Expand Down Expand Up @@ -1953,6 +1963,7 @@ function shellFromState(input: {
archivedAt: input.state.thread.archivedAt,
settledOverride: input.state.thread.settledOverride,
settledAt: input.state.thread.settledAt,
...settleReasonForShell(input.state.thread),
unsettledAt: input.state.thread.unsettledAt ?? null,
snoozedUntil: input.state.thread.snoozedUntil ?? null,
snoozedAt: input.state.thread.snoozedAt ?? null,
Expand Down
9 changes: 6 additions & 3 deletions apps/server/src/orchestration-v2/SubagentProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {

import * as SqlitePersistence from "../persistence/Sqlite.ts";
import * as ProjectionStore from "./ProjectionStore.ts";
import { resolveAutoSettlementAt } from "./ThreadSettlementService.ts";
import { resolveAutoSettlement } from "./ThreadSettlementService.ts";

const parentThreadId = ThreadId.make("thread:subagent-snoozed-parent");
const childThreadId = ThreadId.make("thread:subagent-awake-child");
Expand Down Expand Up @@ -173,14 +173,17 @@ it.effect.each(["provider", "mcp"] as const)(
candidates.map((candidate) => candidate.id),
[childThreadId],
);
const settledAt = resolveAutoSettlementAt({
const settlement = resolveAutoSettlement({
thread: candidates[0]!,
pullRequest: null,
nowMs: DateTime.toEpochMillis(DateTime.add(childCreatedAt, { days: 4 })),
autoSettleAfterDays: 3,
autoSettleOnMerge: false,
});
assert.equal(settledAt && DateTime.formatIso(settledAt), DateTime.formatIso(childCreatedAt));
assert.equal(
settlement && DateTime.formatIso(settlement.settledAt),
DateTime.formatIso(childCreatedAt),
);
}).pipe(
Effect.provide(ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory))),
),
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/orchestration-v2/ThreadManagementService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ it.effect.each([
status: "running" as OrchestrationV2Run["status"],
userMessageId: MessageId.make("follow-up"),
};
const dispatched: Array<string> = [];
const dispatched: Array<unknown> = [];
// The notification starts after the completed run was observed, before settlement commits.
let raced = false;
const layerTest = ThreadManagementService.layer.pipe(
Expand Down Expand Up @@ -661,7 +661,7 @@ it.effect.each([
detail: "Thread is still running",
});
}
dispatched.push(command.type);
dispatched.push(command);
return { sequence: 1, storedEvents: [] } as never;
}),
}),
Expand Down Expand Up @@ -699,7 +699,11 @@ it.effect.each([
} as unknown as OrchestrationV2StoredEvent);
}
yield* Fiber.join(fiber);
expect(dispatched).toEqual(settles ? ["thread.settle"] : []);
expect(dispatched).toEqual(
settles
? [expect.objectContaining({ type: "thread.settle", reason: { type: "agent" } })]
: [],
);
}),
);

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/ThreadManagementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ const make = Effect.gen(function* () {
commandId: CommandId.make(`server:settle-after-run:${input.runId}:${sequence}`),
threadId: input.threadId,
expectedLatestRunId: latest.id,
reason: { type: "agent" },
}).pipe(
Effect.as(true),
// A notification can start between the projection read and the guarded dispatch.
Expand Down Expand Up @@ -1130,6 +1131,7 @@ const make = Effect.gen(function* () {
type: "thread.settle",
commandId: input.commandId,
threadId: input.threadId,
reason: { type: "agent" },
});
return { sequence: result.sequence };
});
Expand Down
Loading
Loading