fix: harden compaction status gating (#1466) (thanks @dlauer)
This commit is contained in:
parent
fd3c76cad3
commit
2e089ec9a0
@ -28,6 +28,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||||
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||||
|
- Compaction: harden oversized summarization handling and gate UI compaction status by verbose level. (#1466) Thanks @dlauer.
|
||||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||||
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
||||||
|
|||||||
@ -15,6 +15,15 @@ const TURN_PREFIX_INSTRUCTIONS =
|
|||||||
const MAX_TOOL_FAILURES = 8;
|
const MAX_TOOL_FAILURES = 8;
|
||||||
const MAX_TOOL_FAILURE_CHARS = 240;
|
const MAX_TOOL_FAILURE_CHARS = 240;
|
||||||
|
|
||||||
|
function isAbortError(err: unknown): boolean {
|
||||||
|
if (!err || typeof err !== "object") return false;
|
||||||
|
const name = "name" in err ? String(err.name) : "";
|
||||||
|
if (name === "AbortError") return true;
|
||||||
|
const message =
|
||||||
|
"message" in err && typeof err.message === "string" ? err.message.toLowerCase() : "";
|
||||||
|
return message.includes("aborted");
|
||||||
|
}
|
||||||
|
|
||||||
type ToolFailure = {
|
type ToolFailure = {
|
||||||
toolCallId: string;
|
toolCallId: string;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@ -251,6 +260,9 @@ async function summarizeWithFallback(params: {
|
|||||||
try {
|
try {
|
||||||
return await summarizeChunks(params);
|
return await summarizeChunks(params);
|
||||||
} catch (fullError) {
|
} catch (fullError) {
|
||||||
|
if (params.signal.aborted || isAbortError(fullError)) {
|
||||||
|
throw fullError;
|
||||||
|
}
|
||||||
console.warn(
|
console.warn(
|
||||||
`Full summarization failed, trying partial: ${
|
`Full summarization failed, trying partial: ${
|
||||||
fullError instanceof Error ? fullError.message : String(fullError)
|
fullError instanceof Error ? fullError.message : String(fullError)
|
||||||
@ -283,6 +295,9 @@ async function summarizeWithFallback(params: {
|
|||||||
const notes = oversizedNotes.length > 0 ? `\n\n${oversizedNotes.join("\n")}` : "";
|
const notes = oversizedNotes.length > 0 ? `\n\n${oversizedNotes.join("\n")}` : "";
|
||||||
return partialSummary + notes;
|
return partialSummary + notes;
|
||||||
} catch (partialError) {
|
} catch (partialError) {
|
||||||
|
if (params.signal.aborted || isAbortError(partialError)) {
|
||||||
|
throw partialError;
|
||||||
|
}
|
||||||
console.warn(
|
console.warn(
|
||||||
`Partial summarization also failed: ${
|
`Partial summarization also failed: ${
|
||||||
partialError instanceof Error ? partialError.message : String(partialError)
|
partialError instanceof Error ? partialError.message : String(partialError)
|
||||||
|
|||||||
@ -173,7 +173,7 @@ export function createAgentEventHandler({
|
|||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldEmitToolEvents = (runId: string, sessionKey?: string) => {
|
const shouldEmitVerboseEvents = (runId: string, sessionKey?: string) => {
|
||||||
const runContext = getAgentRunContext(runId);
|
const runContext = getAgentRunContext(runId);
|
||||||
const runVerbose = normalizeVerboseLevel(runContext?.verboseLevel);
|
const runVerbose = normalizeVerboseLevel(runContext?.verboseLevel);
|
||||||
if (runVerbose) return runVerbose === "on";
|
if (runVerbose) return runVerbose === "on";
|
||||||
@ -198,7 +198,10 @@ export function createAgentEventHandler({
|
|||||||
// Include sessionKey so Control UI can filter tool streams per session.
|
// Include sessionKey so Control UI can filter tool streams per session.
|
||||||
const agentPayload = sessionKey ? { ...evt, sessionKey } : evt;
|
const agentPayload = sessionKey ? { ...evt, sessionKey } : evt;
|
||||||
const last = agentRunSeq.get(evt.runId) ?? 0;
|
const last = agentRunSeq.get(evt.runId) ?? 0;
|
||||||
if (evt.stream === "tool" && !shouldEmitToolEvents(evt.runId, sessionKey)) {
|
if (
|
||||||
|
(evt.stream === "tool" || evt.stream === "compaction") &&
|
||||||
|
!shouldEmitVerboseEvents(evt.runId, sessionKey)
|
||||||
|
) {
|
||||||
agentRunSeq.set(evt.runId, evt.seq);
|
agentRunSeq.set(evt.runId, evt.seq);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,22 @@ describe("gateway server agent", () => {
|
|||||||
{
|
{
|
||||||
registerAgentRunContext("run-tool-off", { sessionKey: "agent:main:main" });
|
registerAgentRunContext("run-tool-off", { sessionKey: "agent:main:main" });
|
||||||
|
|
||||||
|
const compactionEvtP = onceMessage(
|
||||||
|
ws,
|
||||||
|
(o) =>
|
||||||
|
o.type === "event" &&
|
||||||
|
o.event === "agent" &&
|
||||||
|
o.payload?.runId === "run-tool-off" &&
|
||||||
|
o.payload?.stream === "compaction",
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
emitAgentEvent({
|
||||||
|
runId: "run-tool-off",
|
||||||
|
stream: "compaction",
|
||||||
|
data: { phase: "start" },
|
||||||
|
});
|
||||||
|
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId: "run-tool-off",
|
runId: "run-tool-off",
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
@ -98,6 +114,8 @@ describe("gateway server agent", () => {
|
|||||||
? (evt.payload as Record<string, unknown>)
|
? (evt.payload as Record<string, unknown>)
|
||||||
: {};
|
: {};
|
||||||
expect(payload.stream).toBe("assistant");
|
expect(payload.stream).toBe("assistant");
|
||||||
|
|
||||||
|
await expect(compactionEvtP).rejects.toThrow("timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -142,6 +142,7 @@ export type CompactionStatus = {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
startedAt: number | null;
|
startedAt: number | null;
|
||||||
completedAt: number | null;
|
completedAt: number | null;
|
||||||
|
retryingAt: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CompactionHost = ToolStreamHost & {
|
type CompactionHost = ToolStreamHost & {
|
||||||
@ -154,6 +155,7 @@ const COMPACTION_TOAST_DURATION_MS = 5000;
|
|||||||
export function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) {
|
export function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) {
|
||||||
const data = payload.data ?? {};
|
const data = payload.data ?? {};
|
||||||
const phase = typeof data.phase === "string" ? data.phase : "";
|
const phase = typeof data.phase === "string" ? data.phase : "";
|
||||||
|
const willRetry = Boolean(data.willRetry);
|
||||||
|
|
||||||
// Clear any existing timer
|
// Clear any existing timer
|
||||||
if (host.compactionClearTimer != null) {
|
if (host.compactionClearTimer != null) {
|
||||||
@ -166,12 +168,14 @@ export function handleCompactionEvent(host: CompactionHost, payload: AgentEventP
|
|||||||
active: true,
|
active: true,
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
|
retryingAt: null,
|
||||||
};
|
};
|
||||||
} else if (phase === "end") {
|
} else if (phase === "end") {
|
||||||
host.compactionStatus = {
|
host.compactionStatus = {
|
||||||
active: false,
|
active: willRetry,
|
||||||
startedAt: host.compactionStatus?.startedAt ?? null,
|
startedAt: host.compactionStatus?.startedAt ?? null,
|
||||||
completedAt: Date.now(),
|
completedAt: willRetry ? null : Date.now(),
|
||||||
|
retryingAt: willRetry ? Date.now() : null,
|
||||||
};
|
};
|
||||||
// Auto-clear the toast after duration
|
// Auto-clear the toast after duration
|
||||||
host.compactionClearTimer = window.setTimeout(() => {
|
host.compactionClearTimer = window.setTimeout(() => {
|
||||||
@ -183,13 +187,19 @@ export function handleCompactionEvent(host: CompactionHost, payload: AgentEventP
|
|||||||
|
|
||||||
export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {
|
export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
|
|
||||||
// Handle compaction events
|
// Handle compaction events
|
||||||
if (payload.stream === "compaction") {
|
if (payload.stream === "compaction") {
|
||||||
|
const sessionKey =
|
||||||
|
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
||||||
|
if (sessionKey && sessionKey !== host.sessionKey) return;
|
||||||
|
if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;
|
||||||
|
if (host.chatRunId && payload.runId !== host.chatRunId) return;
|
||||||
|
if (!host.chatRunId) return;
|
||||||
handleCompactionEvent(host as CompactionHost, payload);
|
handleCompactionEvent(host as CompactionHost, payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.stream !== "tool") return;
|
if (payload.stream !== "tool") return;
|
||||||
const sessionKey =
|
const sessionKey =
|
||||||
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export type CompactionIndicatorStatus = {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
startedAt: number | null;
|
startedAt: number | null;
|
||||||
completedAt: number | null;
|
completedAt: number | null;
|
||||||
|
retryingAt: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatProps = {
|
export type ChatProps = {
|
||||||
@ -80,6 +81,17 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.retryingAt) {
|
||||||
|
const elapsed = Date.now() - status.retryingAt;
|
||||||
|
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
|
||||||
|
return html`
|
||||||
|
<div class="callout info compaction-indicator compaction-indicator--active">
|
||||||
|
🧹 Retrying compaction...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show "compaction complete" briefly after completion
|
// Show "compaction complete" briefly after completion
|
||||||
if (status.completedAt) {
|
if (status.completedAt) {
|
||||||
const elapsed = Date.now() - status.completedAt;
|
const elapsed = Date.now() - status.completedAt;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user