fix: harden birth-time consultation modes

This commit is contained in:
Jesse_Chen
2026-07-21 07:25:02 +08:00
parent fc1e9f1085
commit 8be39d5ad6
21 changed files with 1516 additions and 206 deletions
@@ -50,6 +50,7 @@ type ControllerInput = Readonly<{
send?: (command: ConversationalRectificationCommand) => Promise<ConversationalRectificationTurn>;
createActionId?: () => string;
onTurn?: (turn: ConversationalRectificationTurn) => void;
onPendingChange?: (pending: boolean) => void;
}>;
type Mutation = Readonly<{
@@ -76,6 +77,9 @@ function createLatestControllerInput(initial: ControllerInput) {
onTurn(turn: ConversationalRectificationTurn) {
current.onTurn?.(turn);
},
onPendingChange(pending: boolean) {
current.onPendingChange?.(pending);
},
};
}
@@ -115,6 +119,15 @@ export function createConversationalRectificationController(
const patch = (next: Partial<ConversationalRectificationControllerSnapshot>) => {
publish({ ...snapshot, ...next });
};
const setPending = (pending: boolean) => {
if (snapshot.pending === pending) return;
patch({ pending });
try {
input.onPendingChange?.(pending);
} catch {
// Parent locks are observational and cannot change durable mutation state.
}
};
const acceptTurn = (
turn: ConversationalRectificationTurn,
clearDraft: boolean,
@@ -160,7 +173,8 @@ export function createConversationalRectificationController(
}));
const run = (mutation: Mutation): MutationResult => {
if (activeMutation?.caseContext === caseContext) return activeMutation.promise;
patch({ pending: true, error: "" });
patch({ error: "" });
setPending(true);
const turnAtStart = snapshot.turn;
const caseContextAtStart = caseContext;
const mutationToken = Symbol("conversational-rectification-mutation");
@@ -190,7 +204,7 @@ export function createConversationalRectificationController(
.finally(() => {
if (activeMutation?.token !== mutationToken) return;
activeMutation = null;
if (caseContext === caseContextAtStart) patch({ pending: false });
if (caseContext === caseContextAtStart) setPending(false);
});
activeMutation = {
caseContext: caseContextAtStart,
@@ -241,6 +255,11 @@ export function createConversationalRectificationController(
if (current === null) return;
caseContext += 1;
activeMutation = null;
try {
input.onPendingChange?.(false);
} catch {
// Parent locks are observational.
}
patch({
turn: null,
draft: "",
@@ -254,6 +273,11 @@ export function createConversationalRectificationController(
if (current === null || current.caseId !== turn.caseId) {
caseContext += 1;
activeMutation = null;
try {
input.onPendingChange?.(false);
} catch {
// Parent locks are observational.
}
patch({
turn,
draft: "",
@@ -388,6 +412,7 @@ export function useConversationalRectification(
createActionId: input.createActionId,
send: latestInput.send,
onTurn: latestInput.onTurn,
onPendingChange: latestInput.onPendingChange,
}));
const snapshot = useSyncExternalStore(
controller.subscribe,