feat: elegant cancellation (#620)
This commit is contained in:
10
bun.lock
10
bun.lock
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@letta-ai/letta-code",
|
||||
"dependencies": {
|
||||
"@letta-ai/letta-client": "^1.6.8",
|
||||
"@letta-ai/letta-client": "^1.7.2",
|
||||
"glob": "^13.0.0",
|
||||
"ink-link": "^5.0.0",
|
||||
"open": "^10.2.0",
|
||||
@@ -14,6 +15,7 @@
|
||||
"@types/bun": "latest",
|
||||
"@types/diff": "^8.0.0",
|
||||
"@types/picomatch": "^4.0.2",
|
||||
"@types/react": "^19.2.9",
|
||||
"diff": "^8.0.2",
|
||||
"husky": "9.1.7",
|
||||
"ink": "^5.0.0",
|
||||
@@ -89,7 +91,7 @@
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@letta-ai/letta-client": ["@letta-ai/letta-client@1.6.8", "", {}, "sha512-FscN1/c03uGByxD2nnnQ6/DMch234UQVlZJcoamU7ElFqv0ESNXb/3zpjL/SreTEUqk7IzrAp2bw5u/ILMt0UA=="],
|
||||
"@letta-ai/letta-client": ["@letta-ai/letta-client@1.7.2", "", {}, "sha512-88XLWwacqTcqL1uZ8QCKmxPIzTlgHAPVbyxVrNLMKeDdqp6Vs7y1WIRFBdEvGy2WJmUgOt5HTUJSnJZevhPX7A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||
|
||||
@@ -99,7 +101,7 @@
|
||||
|
||||
"@types/picomatch": ["@types/picomatch@4.0.2", "", {}, "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||
|
||||
"@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="],
|
||||
|
||||
@@ -139,7 +141,7 @@
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
|
||||
@@ -1014,6 +1014,16 @@ export default function App({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Cleanup abort timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortTimeoutIdRef.current) {
|
||||
clearTimeout(abortTimeoutIdRef.current);
|
||||
abortTimeoutIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Show exit stats on exit (double Ctrl+C)
|
||||
const [showExitStats, setShowExitStats] = useState(false);
|
||||
|
||||
@@ -1038,6 +1048,9 @@ export default function App({
|
||||
// AbortController for stream cancellation
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Timeout for graceful cancellation (waits for stop_reason before force-aborting)
|
||||
const abortTimeoutIdRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Track if user wants to cancel (persists across state updates)
|
||||
const userCancelledRef = useRef(false);
|
||||
|
||||
@@ -2199,6 +2212,12 @@ export default function App({
|
||||
|
||||
// Case 1.5: Stream was cancelled by user
|
||||
if (stopReasonToHandle === "cancelled") {
|
||||
// Clear the force-abort timeout since we received graceful cancellation
|
||||
if (abortTimeoutIdRef.current) {
|
||||
clearTimeout(abortTimeoutIdRef.current);
|
||||
abortTimeoutIdRef.current = null;
|
||||
}
|
||||
|
||||
setStreaming(false);
|
||||
|
||||
// Check if this cancel was triggered by queue threshold
|
||||
@@ -3266,11 +3285,18 @@ export default function App({
|
||||
// Mark any running subagents as interrupted
|
||||
interruptActiveSubagents(INTERRUPTED_BY_USER);
|
||||
|
||||
// NOW abort the stream - interrupted flag is already set
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null; // Clear ref so isAgentBusy() returns false
|
||||
}
|
||||
// DON'T abort immediately - wait for server to send stop_reason: cancelled
|
||||
// This gives server time to gracefully shut down and avoids GeneratorExit errors
|
||||
// Set timeout as safety net in case server doesn't respond
|
||||
const abortTimeoutId = setTimeout(() => {
|
||||
if (abortControllerRef.current) {
|
||||
console.warn("[EAGER_CANCEL] Forcing abort after 30s timeout");
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
|
||||
abortTimeoutIdRef.current = abortTimeoutId;
|
||||
|
||||
// Set cancellation flag to prevent processConversation from starting
|
||||
userCancelledRef.current = true;
|
||||
|
||||
Reference in New Issue
Block a user