mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
837a3daa58
* ♻️ refactor(chat-store): useFetchMessages accepts options object LOBE-9501 Replace the positional `skipFetch?: boolean` second argument with an `options?: { skipFetch?, revalidateOnFocus? }` object on both `useChatStore.useFetchMessages` and `useConversationStore.useFetchMessages`. Plumb `revalidateOnFocus` through to the underlying SWR config so callers can suppress focus revalidate per-call (default behaviour unchanged). Mechanically migrate all 7 call sites to the new shape. No behaviour change in this commit — the streaming-aware `revalidateOnFocus: false` follow-up lives in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(chat): consume gateway uiMessages snapshot as SoT at step boundaries LOBE-9501 Server attaches the canonical UIChatMessage[] snapshot to step_start and agent_runtime_end events (#15152). The client now uses that pushed payload as the source of truth instead of refetching from DB: - step_start handler calls replaceMessages(uiMessages, { context }) when the snapshot is present, so the assistant tab-switch / next-step path no longer issues a refetch that returns a stale assistant placeholder. - agent_runtime_end handler does the same for the terminal step — the last step has no later step_start to carry a fresh snapshot, so this branch is the only one that reconciles the final commit. - step_complete on phase=tool_execution stops calling refreshMessages. That refetch was the direct cause of the assistantGroup→assistant clobber regression captured by the agent-gateway probe scripts. - ChatList disables SWR revalidateOnFocus while the current topic is streaming (via operationSelectors.isAgentRuntimeRunningByContext) and automatically restores it after the run ends. Tab-focus during a run no longer triggers the stale DB read. Doesn't touch streamingExecutor.ts (homogeneous runtime — parallel path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(chat-store): wire gateway handler to consume server-pushed uiMessages SoT LOBE-9501 #15152 (server) attaches the canonical UIChatMessage[] snapshot to both the Redis SSE channel and the gateway /push-event channel. The earlier client patch wired the consumer into `runAgent.ts`, but that file only runs on the Group Chat SSE path. The actual gateway entry point (`createGatewayEventHandler` in `gatewayEventHandler.ts`, used by single agent, sub-agent, and hetero-CLI flows) ignored the field entirely and kept refetching from DB. Fix the gateway handler: - step_start: consume `event.data.uiMessages` and replaceMessages with the pushed SoT. Skipped when absent — hetero adapters don't emit step_start at all (HeterogeneousEventType excludes it), so the new branch is invisible to hetero. - agent_runtime_end: same SoT consumption; the existing `fetchAndReplaceMessages` becomes the fallback for events without the field. Claude Code adapter emits agent_runtime_end with empty data, so hetero terminal behavior is preserved by the fallback. - stream_start: gate the DB fetch on `!newAssistantMessageId`. Native gateway streams carry `assistantMessage.id` (the preceding step_start also delivered the SoT), so the await is unnecessary — AND it was blocking the enqueue chain. Live chunks queued behind that await could not dispatch, which manifested as "streaming content never lands in messagesMap" during tab-switch and slow-network repros. Hetero CLI streams never set `assistantMessage.id`, so the fetch still runs for them on every stream_start. Verified with the agent-gateway probe (separate commit): chunks now land in real time (cLen grows 3 → 529 monotonically), and tab-switch mid-stream no longer rolls the streamed assistantGroup back to the LOADING placeholder (ROLLBACKS=none in the analyzer output). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🧪 chore(local-testing): rewrite agent-gateway probes in TS + add CLI LOBE-9501 Convert the local-testing agent-gateway probes from .js/.mjs to TypeScript and add a unified `run.ts` CLI that bundles via Bun.build (no extra deps) and persists dumps to a gitignored `.agent-gateway/` directory for use as streaming-replay test fixtures. - types.ts: shared dump shape (ProbeStreamEvent / ProbeTimelineSample / ProbeDump) and `declare global` for the `window.__PROBE_*` surface - probe-events.ts: WebSocket + fetch interception (gateway WS captures any socket with `operationId=`; fetch captures `/api/agent/stream` for direct SSE). Per-key timeline samples every 200ms so we can see which messagesMap key streaming chunks actually land in - probe-dump.ts: stops the timeline timer and stashes JSON dump on `window.__PROBE_LAST_DUMP_JSON` (runner returns that global) - analyze-events.ts: stream events (non-chunk) + chunks summary + action-call stacks + correlation + per-key assistant growth + rollback detection. Per-key growth was added specifically to diagnose "chunks arrive but assistant cLen never moves" - run.ts: `install` | `dump [name]` | `analyze [path]` CLI. Bundles via Bun.build, wraps as IIFE with explicit return, pipes to `agent-browser eval --stdin`. Dumps land at `.agent-gateway/<name>-<YYYYMMDD-HHmmss>.json` `.agent-gateway/` is gitignored so dumps accumulate across debugging sessions without polluting git. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🐛 fix(local-testing): repair run.ts after autofix mangled path imports LOBE-9501 The eslint --fix run during the previous commit applied the unicorn `import-style` rule and renamed every `join(` / `dirname(` / `resolve(` to `path.join(` / `path.dirname(` / `path.resolve(`, but the replacement was a naive text substitution that: 1. rewrote `array.join('\n')` to `array.path.join('\n')` — broke bundle error reporting (would TypeError on the build-failure path) 2. produced `const path = path.join(DUMP_DIR, filename)` inside cmdDump — shadowed the `path` module with itself, ReferenceError on every dump invocation Rename the local `path` to `dumpPath` and drop the spurious `.path` prefix on the array `.join`. Verified round-trip: install + dump now write a valid capture to `.agent-gateway/`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🧪 chore(local-testing): capture per-call message snapshot in probe LOBE-9501 The probe's `replaceMessages` wrapper used to record only `count` and `params` — enough to see "two messages were written" but not WHICH two. For post-stream collapse debugging we need to see whether each call restored streamed content (cLen=N) or wiped to LOADING_FLAT (cLen=3). Two changes: - Capture `snapshot` field on every replaceMessages call: last 2 messages' id / role / cLen / rLen / updatedAt. The analyzer prints this inline next to each call so reviewers can see content drift / collapse without re-reading the dump. - Make wrapping idempotent across re-installs. The old guard `chat.__probeWrapped = true` froze the first-installed wrapper across re-installs, so updates to the probe body had no effect without a page reload. Stash the originals on `window.__PROBE_ORIG_REFRESH_MESSAGES` / `window.__PROBE_ORIG_REPLACE_MESSAGES` and re-wrap from those on every install. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🧪 chore(local-testing): add mutation log + dispatchMessage wrap to probe LOBE-9501 The replaceMessages-only wrap couldn't catch chunk-level writes (those go through internal_dispatchMessage) or attribute post-stream collapses to a specific writer. Add: - `__PROBE_MUTATIONS` — unified ordered log of every dbMessagesMap[key] reference change, with `last`/`prevLast` summaries and a `delta` field that tags interesting transitions (`cLen↓N→M`, `rLen↓`, `id:A→B`, `n↓prev→cur`). Both writers — replaceMessages AND internal_dispatchMessage — push to the same buffer so a single timeline shows all stores writes. - Idempotent action wrapping. Originals are stashed on `window.__PROBE_ORIG_*` and re-wrapped from there on every install, so probe edits take effect without a page reload (previous `chat.__probeWrapped` flag froze the first wrapper). - Snapshot field on replaceMessages — last 2 messages' id/role/cLen/rLen/updatedAt — so reviewers can see WHICH content each call is writing instead of just the count. - Dump file now carries the `mutations` array alongside streamEvents, actionCalls, timeline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🐛 fix(chat-store): gate SWR onData by isStreaming for streaming topic LOBE-9501 Backstop for the post-stream cLen collapse that survives even with the gateway SoT consume in place. Reproduction (confirmed): 1. Send a stream that lands lots of WS chunks into ChatStore 2. Immediately reload the page If the page reload races against server-side chunk fan-out into Postgres, SWR's fresh fetch returns the assistant row in its LOADING_FLAT placeholder state (cLen=3) and writes that to ChatStore via the conversation-store mirror — even though the WS push at agent_runtime_end carried the correct full content moments earlier. `mergeFetchedMessagesWithLocalState`'s updatedAt tie-breaker handles this for in-session repros (local message wins when its updatedAt is newer), but it degenerates when: - The SoT consume just wrote server's snapshot updatedAt onto the local message, equalising the timestamps so the next stale DB fetch wins - The user reloads (no local state to merge against — fresh fetch wins outright) Add a gate at the bottom of `ConversationStore.useFetchMessages.onData`: while `isAgentRuntimeRunningByContext(context)` is true, drop the SWR write entirely. SWR's own cache still updates, so once streaming ends a normal revalidate writes through correctly. This is layered defense — it does NOT fix the underlying server-side fan-out lag (filed as separate Linear issue). It does prevent the client-side flash users currently see during the lag window. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🧪 test(chat-store): align gateway handler tests with SoT contract The previous assertions still expected `stream_start` to issue a DB refetch on every native gateway stream — the very behaviour LOBE-9501 removes (`acb9523a04`). Update the three failing cases to the new contract: - `stream_start > should associate new message with operation`: assert `messageService.getMessages` is NOT called when `assistantMessage.id` is present (the SoT snapshot from the preceding `step_start` already pre-populated `dbMessagesMap`). - `sequential processing`: rewrite around the surviving ordering guarantee — `associate` (stream_start) must precede `dispatch` (stream_chunk) so the chunk targets the new id. Add a sibling case for hetero CLI streams (no `assistantMessage.id` → DB fetch is still mandatory). - `multi-step integration > full LLM → tools → LLM cycle`: keep the post-`tool_end` `replaceMessages` assertion (tool_end still refreshes from DB), invert the post-`stream_start` assertion for step 2. 42 tests passing (was 41 + 1 new hetero fallback test). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 lines
1.1 KiB
TypeScript
38 lines
1.1 KiB
TypeScript
// Stops the events-probe timeline timer and stashes the full capture as a
|
|
// JSON string on `window.__PROBE_LAST_DUMP_JSON`. `run.ts` wraps the bundle
|
|
// in an IIFE that returns that global, which `agent-browser eval` prints to
|
|
// stdout — the runner then persists it under `.agent-gateway/`.
|
|
|
|
import type { ProbeDump } from './types';
|
|
|
|
declare global {
|
|
interface Window {
|
|
__PROBE_LAST_DUMP_JSON?: string;
|
|
}
|
|
}
|
|
|
|
const w = window;
|
|
|
|
if (w.__PROBE_TIMELINE_TIMER) {
|
|
clearInterval(w.__PROBE_TIMELINE_TIMER);
|
|
w.__PROBE_TIMELINE_TIMER = null;
|
|
}
|
|
|
|
const mutations = w.__PROBE_MUTATIONS ?? [];
|
|
|
|
const dump: ProbeDump & { mutations: typeof mutations } = {
|
|
meta: {
|
|
t0: w.__PROBE_T0 ?? 0,
|
|
collectedAt: Date.now(),
|
|
sampleCount: (w.__PROBE_MSG_TIMELINE ?? []).length,
|
|
eventCount: (w.__PROBE_STREAM_EVENTS ?? []).length,
|
|
callCount: (w.__PROBE_ACTION_CALLS ?? []).length,
|
|
},
|
|
streamEvents: w.__PROBE_STREAM_EVENTS ?? [],
|
|
actionCalls: w.__PROBE_ACTION_CALLS ?? [],
|
|
timeline: w.__PROBE_MSG_TIMELINE ?? [],
|
|
mutations,
|
|
};
|
|
|
|
w.__PROBE_LAST_DUMP_JSON = JSON.stringify(dump);
|