upgrade openclaw to 5.19 (#1063)

This commit is contained in:
paisley
2026-05-25 17:48:12 +08:00
committed by GitHub
parent 219db5ff78
commit d15e2330d5
7 changed files with 436 additions and 552 deletions

View File

@@ -0,0 +1,37 @@
---
id: fix-chat-stuck-processing-tool-results
title: Clear stale chat run state when Gateway reports the session is idle
scenario: gateway-backend-communication
taskType: runtime-bridge
intent: Prevent the renderer from staying stuck on “Processing tool results…” when a terminal Gateway lifecycle event was missed but sessions.list reports the current session has no active run.
touchedAreas:
- harness/specs/tasks/fix-chat-stuck-processing-tool-results.md
- package.json
- pnpm-lock.yaml
- src/stores/chat.ts
- src/stores/chat/session-actions.ts
- src/stores/chat/types.ts
- tests/unit/chat-session-actions.test.ts
expectedUserBehavior:
- If Gateway reports the current session has status done or hasActiveRun=false after the user's send timestamp, the chat composer and tool-processing indicator return to idle.
- A fresh in-flight send is not prematurely cleared by stale sessions.list metadata from before the user message.
- Renderer continues to use the existing gateway:rpc Main-process boundary.
requiredProfiles:
- fast
- comms
requiredRules:
- gateway-readiness-policy
- renderer-main-boundary
- backend-communication-boundary
- api-client-transport-policy
requiredTests:
- pnpm exec vitest run tests/unit/chat-session-actions.test.ts
- pnpm run typecheck
acceptance:
- Renderer does not add direct IPC calls outside the existing api-client invocation path.
- Renderer does not fetch Gateway HTTP directly.
- sessions.list idle metadata reconciles stale sending/activeRunId/pendingFinal state for the current session.
- sessions.list metadata older than the current user send does not clear active state.
docs:
required: false
---

View File

@@ -92,12 +92,12 @@
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.7",
"@larksuite/openclaw-lark": "2026.5.12",
"@larksuite/openclaw-lark": "2026.5.13",
"@larksuiteoapi/node-sdk": "^1.61.1",
"@monaco-editor/react": "^4.7.0",
"@openclaw/discord": "2026.5.12",
"@openclaw/qqbot": "2026.5.12",
"@openclaw/whatsapp": "2026.5.12",
"@openclaw/discord": "2026.5.19",
"@openclaw/qqbot": "2026.5.19",
"@openclaw/whatsapp": "2026.5.19",
"@playwright/test": "^1.56.1",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -112,7 +112,7 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@sinclair/typebox": "^0.34.48",
"@soimy/dingtalk": "^3.6.2",
"@soimy/dingtalk": "^3.6.3",
"@tencent-connect/qqbot-connector": "^1.1.0",
"@tencent-weixin/openclaw-weixin": "^2.4.3",
"@testing-library/jest-dom": "^6.9.1",
@@ -125,7 +125,7 @@
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-react": "^5.1.4",
"@wecom/wecom-openclaw-plugin": "^2026.5.7",
"@wecom/wecom-openclaw-plugin": "^2026.5.14",
"@whiskeysockets/baileys": "7.0.0-rc.9",
"acpx": "0.5.3",
"autoprefixer": "^10.4.24",
@@ -151,7 +151,7 @@
"monaco-editor": "^0.55.1",
"mpg123-decoder": "^1.0.3",
"ms": "^2.1.3",
"openclaw": "2026.5.12",
"openclaw": "2026.5.19",
"opusscript": "^0.1.1",
"pdfjs-dist": "^5.7.284",
"playwright-core": "1.59.1",

755
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1404,6 +1404,56 @@ function parseSessionUpdatedAtMs(value: unknown): number | undefined {
return undefined;
}
function parseSessionStatus(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : undefined;
}
function sessionIndicatesIdle(session: ChatSession | undefined): boolean {
if (!session) return false;
if (session.hasActiveRun === false) return true;
return session.status === 'done'
|| session.status === 'completed'
|| session.status === 'finished'
|| session.status === 'failed'
|| session.status === 'error'
|| session.status === 'aborted'
|| session.status === 'cancelled';
}
function reconcileCurrentSessionIdleFromBackend(
set: (partial: Partial<ChatState> | ((state: ChatState) => Partial<ChatState>)) => void,
get: () => ChatState,
sessions: ChatSession[],
): void {
const state = get();
if (!state.sending && state.activeRunId == null && !state.pendingFinal) return;
const current = sessions.find((session) => session.key === state.currentSessionKey);
if (!sessionIndicatesIdle(current)) return;
// Avoid clearing a brand-new send from stale sessions.list metadata. The
// backend's session row must have been updated at or after the user message
// that armed the renderer run state.
if (
state.lastUserMessageAt != null
&& typeof current?.updatedAt === 'number'
&& current.updatedAt < toMs(state.lastUserMessageAt)
) {
return;
}
set({
sending: false,
activeRunId: null,
pendingFinal: false,
lastUserMessageAt: null,
streamingText: '',
streamingMessage: null,
streamingTools: [],
pendingToolImages: [],
});
}
async function loadCronFallbackMessages(sessionKey: string, limit = 200): Promise<RawMessage[]> {
if (!isCronSessionKey(sessionKey)) return [];
try {
@@ -2093,6 +2143,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
thinkingLevel: s.thinkingLevel ? String(s.thinkingLevel) : undefined,
model: s.model ? String(s.model) : undefined,
updatedAt: parseSessionUpdatedAtMs(s.updatedAt),
status: parseSessionStatus(s.status),
hasActiveRun: typeof s.hasActiveRun === 'boolean' ? s.hasActiveRun : undefined,
})).filter((s: ChatSession) => s.key);
const canonicalBySuffix = new Map<string, string>();
@@ -2175,6 +2227,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
},
}));
}
reconcileCurrentSessionIdleFromBackend(set, get, sessionsWithCurrent);
applySessionBackendLabels(set, sessionsWithCurrent);
// Background: fetch first user message for every non-main session to populate labels upfront.

View File

@@ -57,6 +57,52 @@ function parseSessionUpdatedAtMs(value: unknown): number | undefined {
return undefined;
}
function parseSessionStatus(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : undefined;
}
function sessionIndicatesIdle(session: ChatSession | undefined): boolean {
if (!session) return false;
if (session.hasActiveRun === false) return true;
return session.status === 'done'
|| session.status === 'completed'
|| session.status === 'finished'
|| session.status === 'failed'
|| session.status === 'error'
|| session.status === 'aborted'
|| session.status === 'cancelled';
}
function reconcileCurrentSessionIdleFromBackend(set: ChatSet, get: ChatGet, sessions: ChatSession[]): void {
const state = get();
if (!state.sending && state.activeRunId == null && !state.pendingFinal) return;
const current = sessions.find((session) => session.key === state.currentSessionKey);
if (!sessionIndicatesIdle(current)) return;
// Avoid clearing a brand-new send from stale sessions.list metadata. The
// backend's session row must have been updated at or after the user message
// that armed the renderer run state.
if (
state.lastUserMessageAt != null
&& typeof current?.updatedAt === 'number'
&& current.updatedAt < toMs(state.lastUserMessageAt)
) {
return;
}
set({
sending: false,
activeRunId: null,
pendingFinal: false,
lastUserMessageAt: null,
streamingText: '',
streamingMessage: null,
streamingTools: [],
pendingToolImages: [],
});
}
export function createSessionActions(
set: ChatSet,
get: ChatGet,
@@ -85,6 +131,8 @@ export function createSessionActions(
thinkingLevel: s.thinkingLevel ? String(s.thinkingLevel) : undefined,
model: s.model ? String(s.model) : undefined,
updatedAt: parseSessionUpdatedAtMs(s.updatedAt),
status: parseSessionStatus(s.status),
hasActiveRun: typeof s.hasActiveRun === 'boolean' ? s.hasActiveRun : undefined,
})).filter((s: ChatSession) => s.key);
const canonicalBySuffix = new Map<string, string>();
@@ -147,6 +195,7 @@ export function createSessionActions(
...discoveredActivity,
},
}));
reconcileCurrentSessionIdleFromBackend(set, get, sessionsWithCurrent);
applySessionBackendLabels(set, sessionsWithCurrent);
const gatewayRuntimeKey = getSessionLabelHydrationRuntimeKey(undefined);

View File

@@ -77,6 +77,8 @@ export interface ChatSession {
thinkingLevel?: string;
model?: string;
updatedAt?: number;
status?: string;
hasActiveRun?: boolean;
}
export interface ToolStatus {

View File

@@ -8,10 +8,11 @@ vi.mock('@/lib/api-client', () => ({
type ChatLikeState = {
currentSessionKey: string;
sessions: Array<{ key: string; displayName?: string; updatedAt?: number }>;
sessions: Array<{ key: string; displayName?: string; updatedAt?: number; status?: string; hasActiveRun?: boolean }>;
messages: Array<{ role: string; timestamp?: number; content?: unknown }>;
sessionLabels: Record<string, string>;
sessionLastActivity: Record<string, number>;
sending: boolean;
streamingText: string;
streamingMessage: unknown | null;
streamingTools: unknown[];
@@ -30,6 +31,7 @@ function makeHarness(initial?: Partial<ChatLikeState>) {
messages: [],
sessionLabels: {},
sessionLastActivity: {},
sending: false,
streamingText: '',
streamingMessage: null,
streamingTools: [],
@@ -173,5 +175,79 @@ describe('chat session actions', () => {
expect(h.read().sessionLastActivity['agent:main:cron:job-1']).toBe(1773281731621);
expect(h.read().sessions.find((session) => session.key === 'agent:main:cron:job-1')?.updatedAt).toBe(1773281731621);
});
it('clears stale current-run state when sessions.list reports the current session is idle', async () => {
const { createSessionActions } = await import('@/stores/chat/session-actions');
const h = makeHarness({
currentSessionKey: 'agent:main:main',
sessions: [{ key: 'agent:main:main' }],
sending: true,
activeRunId: 'run-stale',
pendingFinal: true,
lastUserMessageAt: 1779693769991,
streamingText: 'partial',
streamingMessage: { role: 'assistant', content: 'partial' },
streamingTools: [{ name: 'browser', status: 'completed' }],
pendingToolImages: [{ fileName: 'x.png' }],
});
const actions = createSessionActions(h.set as never, h.get as never);
invokeIpcMock.mockResolvedValueOnce({
success: true,
result: {
sessions: [{
key: 'agent:main:main',
displayName: 'Main',
updatedAt: 1779694521057,
status: 'done',
hasActiveRun: false,
}],
},
});
await actions.loadSessions();
const next = h.read();
expect(next.sending).toBe(false);
expect(next.activeRunId).toBeNull();
expect(next.pendingFinal).toBe(false);
expect(next.lastUserMessageAt).toBeNull();
expect(next.streamingText).toBe('');
expect(next.streamingMessage).toBeNull();
expect(next.streamingTools).toEqual([]);
expect(next.pendingToolImages).toEqual([]);
});
it('does not clear current-run state from stale sessions.list metadata older than the send', async () => {
const { createSessionActions } = await import('@/stores/chat/session-actions');
const h = makeHarness({
currentSessionKey: 'agent:main:main',
sending: true,
activeRunId: 'run-current',
pendingFinal: true,
lastUserMessageAt: 2000,
});
const actions = createSessionActions(h.set as never, h.get as never);
invokeIpcMock.mockResolvedValueOnce({
success: true,
result: {
sessions: [{
key: 'agent:main:main',
updatedAt: 1000,
status: 'done',
hasActiveRun: false,
}],
},
});
await actions.loadSessions();
const next = h.read();
expect(next.sending).toBe(true);
expect(next.activeRunId).toBe('run-current');
expect(next.pendingFinal).toBe(true);
expect(next.lastUserMessageAt).toBe(2000);
});
});