fix(maestro): guard structured web submits

This commit is contained in:
Miguel Palhas
2026-09-05 08:09:51 +01:00
parent c348381c5f
commit b86335ddf1
2 changed files with 165 additions and 10 deletions
+40 -10
View File
@@ -109,6 +109,10 @@ export function createStructuredConnection(
let state: ReducedState = { turn: idleTurn() };
let pendingApprovalRowId: string | null = null;
let latestSession: Session | undefined = store?.snapshot().sessions.get(sessionId);
let submissionGeneration = 0;
let awaitingBeginGeneration: number | null = null;
let acceptedSubmission: { generation: number; socketGeneration: number } | null = null;
let socketGeneration = 0;
function lastRowIdOfKind(kind: TranscriptRowKind): string | null {
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].kind === kind) return rows[i].id;
@@ -217,14 +221,23 @@ export function createStructuredConnection(
}
function renderComposerState(): void {
const active = state.turn.responding || state.turn.thinking;
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
input.disabled = active;
sendBtn.disabled = active;
cancelBtn.hidden = !active;
cancelBtn.hidden = !state.turn.responding && !state.turn.thinking;
}
function applyState(next: ReducedState): void {
function applyState(next: ReducedState, connectionGeneration: number): void {
state = next;
if (state.turn.responding || state.turn.thinking) {
awaitingBeginGeneration = null;
} else if (
awaitingBeginGeneration !== null
&& acceptedSubmission?.generation === awaitingBeginGeneration
&& connectionGeneration > acceptedSubmission.socketGeneration
) {
awaitingBeginGeneration = null;
}
const newPendingId = state.pending_approval ? lastRowIdOfKind("approval") : null;
if (newPendingId !== pendingApprovalRowId) {
const prev = pendingApprovalRowId;
@@ -268,14 +281,30 @@ export function createStructuredConnection(
e.preventDefault();
const text = input.value;
if (!text.trim() || input.disabled) return;
const generation = ++submissionGeneration;
awaitingBeginGeneration = generation;
acceptedSubmission = null;
input.value = "";
autoResize();
clearError();
sendStructuredPrompt(sessionId, text).catch((e: unknown) => {
input.value = text;
autoResize();
showError(String(e));
});
renderComposerState();
sendStructuredPrompt(sessionId, text).then(
() => {
if (generation === submissionGeneration) {
acceptedSubmission = { generation, socketGeneration };
}
},
(e: unknown) => {
if (generation !== submissionGeneration) return;
if (awaitingBeginGeneration === generation) awaitingBeginGeneration = null;
if (input.value === "") {
input.value = text;
autoResize();
}
showError(String(e));
renderComposerState();
},
);
});
cancelBtn.addEventListener("click", () => {
clearError();
@@ -303,6 +332,7 @@ export function createStructuredConnection(
let disposed = false;
function connect(): void {
const connectionGeneration = ++socketGeneration;
ws = socketFactory(sessionId);
ws.onopen = () => {
reconnectMs = INITIAL_RECONNECT_MS;
@@ -321,11 +351,11 @@ export function createStructuredConnection(
applySnapshot(msg.snapshot);
break;
case "reduced_state":
applyState(msg.state);
applyState(msg.state, connectionGeneration);
break;
case "projection_update":
applyDelta(msg.transcript);
applyState(msg.state);
applyState(msg.state, connectionGeneration);
break;
}
};