fix: prevent approval from reappearing after interrupt during execution (#571)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -689,6 +689,8 @@ export default function App({
|
||||
reason: string;
|
||||
}>
|
||||
>([]);
|
||||
const executingToolCallIdsRef = useRef<string[]>([]);
|
||||
const interruptQueuedRef = useRef(false);
|
||||
|
||||
// Bash mode: cache bash commands to prefix next user message
|
||||
// Use ref instead of state to avoid stale closure issues in onSubmit
|
||||
@@ -2718,6 +2720,20 @@ export default function App({
|
||||
if (isExecutingTool && toolAbortControllerRef.current) {
|
||||
toolAbortControllerRef.current.abort();
|
||||
|
||||
if (executingToolCallIdsRef.current.length > 0) {
|
||||
const interruptedResults = executingToolCallIdsRef.current.map(
|
||||
(toolCallId) => ({
|
||||
type: "tool" as const,
|
||||
tool_call_id: toolCallId,
|
||||
tool_return: INTERRUPTED_BY_USER,
|
||||
status: "error" as const,
|
||||
}),
|
||||
);
|
||||
setQueuedApprovalResults(interruptedResults);
|
||||
executingToolCallIdsRef.current = [];
|
||||
interruptQueuedRef.current = true;
|
||||
}
|
||||
|
||||
// ALSO abort the main stream - don't leave it running
|
||||
buffersRef.current.abortGeneration =
|
||||
(buffersRef.current.abortGeneration || 0) + 1;
|
||||
@@ -5683,6 +5699,7 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
approvals: queuedApprovalResults,
|
||||
});
|
||||
setQueuedApprovalResults(null);
|
||||
interruptQueuedRef.current = false;
|
||||
}
|
||||
|
||||
initialInput.push({
|
||||
@@ -5804,6 +5821,10 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
...(additionalDecision ? [additionalDecision] : []),
|
||||
];
|
||||
|
||||
executingToolCallIdsRef.current = allDecisions
|
||||
.filter((decision) => decision.type === "approve")
|
||||
.map((decision) => decision.approval.toolCallId);
|
||||
|
||||
// Set phase to "running" for all approved tools
|
||||
setToolCallsRunning(
|
||||
buffersRef.current,
|
||||
@@ -5898,9 +5919,9 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
const userCancelled = userCancelledRef.current;
|
||||
|
||||
if (wasAborted || userCancelled) {
|
||||
// Queue results to send alongside the next user message (if not cancelled entirely)
|
||||
// Don't queue if ESC was pressed - interrupted results would cause desync errors
|
||||
if (!userCancelled) {
|
||||
// Queue results to send alongside the next user message so the backend
|
||||
// doesn't keep requesting the same approvals after an interrupt.
|
||||
if (!interruptQueuedRef.current) {
|
||||
setQueuedApprovalResults(allResults as ApprovalResult[]);
|
||||
}
|
||||
setStreaming(false);
|
||||
@@ -5921,6 +5942,8 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
// Always release the execution guard, even if an error occurred
|
||||
setIsExecutingTool(false);
|
||||
toolAbortControllerRef.current = null;
|
||||
executingToolCallIdsRef.current = [];
|
||||
interruptQueuedRef.current = false;
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -23,10 +23,13 @@ export const StreamingOutputDisplay = memo(
|
||||
const { tailLines, totalLineCount } = streaming;
|
||||
const hiddenCount = Math.max(0, totalLineCount - tailLines.length);
|
||||
|
||||
// No output yet - don't show anything
|
||||
const firstLine = tailLines[0];
|
||||
if (!firstLine) {
|
||||
return null;
|
||||
return (
|
||||
<Box>
|
||||
<Text dimColor>{` ⎿ Running... (${elapsed}s)`}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages";
|
||||
import { INTERRUPTED_BY_USER } from "../../constants";
|
||||
import { isShellTool } from "./toolNameMapping";
|
||||
|
||||
// Constants for streaming output
|
||||
const MAX_TAIL_LINES = 5;
|
||||
@@ -610,7 +611,23 @@ export function setToolCallsRunning(b: Buffers, toolCallIds: string[]): void {
|
||||
if (lineId) {
|
||||
const line = b.byId.get(lineId);
|
||||
if (line && line.kind === "tool_call") {
|
||||
b.byId.set(lineId, { ...line, phase: "running" });
|
||||
const shouldSeedStreaming =
|
||||
line.name && isShellTool(line.name) && !line.streaming;
|
||||
b.byId.set(lineId, {
|
||||
...line,
|
||||
phase: "running",
|
||||
...(shouldSeedStreaming
|
||||
? {
|
||||
streaming: {
|
||||
tailLines: [],
|
||||
partialLine: "",
|
||||
partialIsStderr: false,
|
||||
totalLineCount: 0,
|
||||
startTime: Date.now(),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ function spawnWithLauncher(
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
let timedOut = false;
|
||||
let killTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
@@ -65,6 +66,13 @@ function spawnWithLauncher(
|
||||
|
||||
const abortHandler = () => {
|
||||
childProcess.kill("SIGTERM");
|
||||
if (!killTimer) {
|
||||
killTimer = setTimeout(() => {
|
||||
if (childProcess.exitCode === null && !childProcess.killed) {
|
||||
childProcess.kill("SIGKILL");
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
if (options.signal) {
|
||||
options.signal.addEventListener("abort", abortHandler, { once: true });
|
||||
@@ -82,6 +90,10 @@ function spawnWithLauncher(
|
||||
|
||||
childProcess.on("error", (err) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (killTimer) {
|
||||
clearTimeout(killTimer);
|
||||
killTimer = null;
|
||||
}
|
||||
if (options.signal) {
|
||||
options.signal.removeEventListener("abort", abortHandler);
|
||||
}
|
||||
@@ -90,6 +102,10 @@ function spawnWithLauncher(
|
||||
|
||||
childProcess.on("close", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (killTimer) {
|
||||
clearTimeout(killTimer);
|
||||
killTimer = null;
|
||||
}
|
||||
if (options.signal) {
|
||||
options.signal.removeEventListener("abort", abortHandler);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user