β¨ feat(agent-runtime): push UIChatMessage snapshot at gateway step boundaries#15152
Conversation
β¦T validation Probe + tab-switch + analyzer scripts under .agents/skills/local-testing/scripts/agent-gateway/ to capture in-browser snapshots of the message store during gateway streaming and detect regressions where assistantGroup messages get clobbered by stale DB refetches. Used to verify LOBE-9501. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦boundaries LOBE-9501 Gateway-mode streaming previously let the client refetch from DB on every step_complete or tab-focus; with stream chunks landing before the DB write fans out, the refetch returned a stale assistant placeholder that clobbered the in-memory streamed assistantGroup (reasoning / tool calls / content). Server now attaches the canonical UIChatMessage[] snapshot to step_start and agent_runtime_end events so the client can use the pushed payload as Source of Truth instead of refetching: - step_start now loads agent state first, queries messages, and attaches uiMessages to the event data when topic context is known - publishAgentRuntimeEnd signature switched to a params object (additive uiMessages field) and the coordinator resolves the snapshot through an optional uiMessagesResolver hook before publishing terminal events - AgentRuntimeService wires the resolver through a lazily-instantiated MessageService so tests without S3 env still construct cleanly - MessageService.queryMessages exposes the same read path as the message.getMessages trpc lambda (FileService postProcessUrl included) Pure additive on the wire: legacy consumers see new uiMessages field, old finalState payload unchanged. Existing call sites in agentNotify and aiAgent migrated to the params shape. Failures in the resolver fall back to publishing without uiMessages so streaming never fails the step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
π‘ Codex Review
publishAgentRuntimeEnd now accepts uiMessages, but the gateway push payload drops that field and only forwards { errorType, finalState, reason, reasonDetail }. In gateway mode, clients consume /push-event data rather than Redis directly, so terminal events lose the canonical snapshot even though the inner stream manager has it. This breaks the new βuse pushed uiMessages as SoTβ flow at operation end (especially the final step where no later step_start exists to carry a fresh snapshot).
βΉοΈ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with π.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codecov Reportβ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## canary #15152 +/- ##
=========================================
Coverage 70.89% 70.89%
=========================================
Files 3144 3144
Lines 312964 313009 +45
Branches 33148 27492 -5656
=========================================
+ Hits 221881 221920 +39
- Misses 90917 90923 +6
Partials 166 166
Flags with carried forward coverage won't be shown. Click here to find out more.
π New features to boost your workflow:
|
LOBE-9501
GatewayStreamNotifier.publishAgentRuntimeEnd was delegating uiMessages to
the inner manager (Redis SSE) but reconstructing its own push-event data
object that only carried { errorType, finalState, reason, reasonDetail }.
In gateway mode, clients consume /push-event rather than Redis directly,
so the canonical UIChatMessage[] snapshot never reached them at terminal
state β and the final step has no later step_start to carry a fresh one.
Forward uiMessages via the same conditional-spread pattern used in the
inner managers; add two tests covering the present/absent branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ssages 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>
β¦daries (#15153) * β»οΈ 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>
β¦undaries (lobehub#15152) * π§ͺ chore(local-testing): add agent-gateway probe scripts for stream SoT validation Probe + tab-switch + analyzer scripts under .agents/skills/local-testing/scripts/agent-gateway/ to capture in-browser snapshots of the message store during gateway streaming and detect regressions where assistantGroup messages get clobbered by stale DB refetches. Used to verify LOBE-9501. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(agent-runtime): push canonical UIChatMessage snapshot at step boundaries LOBE-9501 Gateway-mode streaming previously let the client refetch from DB on every step_complete or tab-focus; with stream chunks landing before the DB write fans out, the refetch returned a stale assistant placeholder that clobbered the in-memory streamed assistantGroup (reasoning / tool calls / content). Server now attaches the canonical UIChatMessage[] snapshot to step_start and agent_runtime_end events so the client can use the pushed payload as Source of Truth instead of refetching: - step_start now loads agent state first, queries messages, and attaches uiMessages to the event data when topic context is known - publishAgentRuntimeEnd signature switched to a params object (additive uiMessages field) and the coordinator resolves the snapshot through an optional uiMessagesResolver hook before publishing terminal events - AgentRuntimeService wires the resolver through a lazily-instantiated MessageService so tests without S3 env still construct cleanly - MessageService.queryMessages exposes the same read path as the message.getMessages trpc lambda (FileService postProcessUrl included) Pure additive on the wire: legacy consumers see new uiMessages field, old finalState payload unchanged. Existing call sites in agentNotify and aiAgent migrated to the params shape. Failures in the resolver fall back to publishing without uiMessages so streaming never fails the step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π fix(agent-runtime): forward uiMessages in gateway /push-event payload LOBE-9501 GatewayStreamNotifier.publishAgentRuntimeEnd was delegating uiMessages to the inner manager (Redis SSE) but reconstructing its own push-event data object that only carried { errorType, finalState, reason, reasonDetail }. In gateway mode, clients consume /push-event rather than Redis directly, so the canonical UIChatMessage[] snapshot never reached them at terminal state β and the final step has no later step_start to carry a fresh one. Forward uiMessages via the same conditional-spread pattern used in the inner managers; add two tests covering the present/absent branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦daries (lobehub#15153) * β»οΈ 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 (lobehub#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 lobehub#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>
# π LobeHub Release (20260528) **Release Date:** May 28, 2026 **Since v2.2.0:** 220 merged PRs Β· 15 contributors > This cycle brings heterogeneous "platform agents" you can dispatch to local or remote devices, a rebuilt onboarding flow, document-centric chat, and a unified model-runtime error model β with new DeepSeek V4 and Gemini 3.5 Flash support along the way. --- ## β¨ Highlights - **More Hetero Agents (OpenClaw / Hermes)** β Create heterogeneous agents and dispatch them to local or remote devices through the device gateway, with an execution-target switcher in the composer and persistent CLI sessions. (#15065, #15179, #15022) - **iMessage on Desktop** β New iMessage setup and bridge on desktop, plus bot attachments across every platform. (#15228, #15227, #15029) - **Skills in the Composer** β Drag skill chips into chat, trigger installed skills from the slash menu mid-line, and surface project-level skills in the homogeneous agent runtime. (#15095, #15061, #15110) - **New Models** β DeepSeek V4 Flash/Pro and Gemini 3.5 Flash across providers, with thinking params for structured output and chat cost estimates. (#15031, #15001, #15051, #14876) - **Agent Runtime Observability** β OpenTelemetry GenAI semantic conventions plus per-call generation tracing. (#15123, #15124) --- ## π€ Agents & Heterogeneous Runtime - **Platform agent creation** β OpenClaw/Hermes creation UI, device guard, and remote dispatch backend. (#15065) - **Execution-target switcher** β Pick local vs remote execution directly in the composer; device-selection UX with actionable guidance. (#15179, #15111) - **CLI hetero dispatch** β OpenClaw/Hermes dispatch with persistent sessions and a notify protocol. (#15022) - **Gateway snapshot as source of truth** β Consume the gateway `uiMessages` snapshot at step boundaries to keep chat state consistent. (#15153, #15152) - **Client sub-agent as a normal tool call** β Simplifies the sub-agent execution path. (#15281) - **Hermes agent chain** β Implements the Hermes agent chain logic. (#15189) - **Device registry** β TRPC endpoints to register, list, update, and remove devices. (#15299) - **Desktop device routing** β Route gateway agent runs through `lh hetero exec`; restore `userId` in gateway dispatch and gate local-system by execution target. (#15132, #15232) - **Agent signals** β Anchor agent-signal receipts to messages and isolate memory-agent messages into a child thread. (#14969, #14921) --- ## π Onboarding - **Simplified first screen** β Defer topic creation to first send. (#15090) - **Market Agent Picker** β Added as a classic onboarding step, with template prefetch. (#14980, #15041) - **Welcome guidance** β Show agent welcome guidance on first run. (#15098) - **Mobile** β Adapt agent onboarding UI and restore Classic-step padding on mobile. (#15019, #15032) - **Discovery** β Streamline discovery to a single profession question. (#14987) - **Analytics** β Track onboarding step events and create-agent modal source. (#15133, #15028) --- ## π Documents, Pages & Knowledge - **Thread chat in preview** β Embed thread chat in the document preview portal. (#15216) - **Non-markdown rendering** β Render non-markdown docs as a read-only highlight. (#15272) - **Multi-select** β Multi-select delete in the document tree. (#15125) - **Page-agent streaming** β Preview `initPage` streaming arguments. (#15039) - **Per-agent topics** β Per-agent topic management page. (#15207) - **Server-side category** β Derive document category server-side and drop frontend predicates. (#15076) --- ## π§© Skills & Tools - **Drag skill chips** β Drag skills into chat input and register agent-document skills. (#15095) - **Slash menu** β Installed skills appear in the slash menu with a mid-line trigger. (#15061) - **Project skills** β Recognize project-level skills in the homogeneous agent runtime and surface them regardless of active device. (#15110, #15177) - **VFS archiving** β Archive oversized tool results to VFS instead of truncating. (#15074) - **@localfile mentions** β Drag folders into chat input as `@localFile` mentions on desktop. (#15071) --- ## π§ Model Runtime & Providers - **Error spec registry** β Unify error codes into a spec + pattern registry, split `ProviderBizError` into finer codes, classify Cloud-only codes via a tier digit, and add `DatabasePersistError`. (#15262, #15286, #15278, #15279) - **New models** β DeepSeek V4 Flash/Pro (opencode-go) and Gemini 3.5 Flash; DeepSeek V4 Pro on SiliconCloud. (#15031, #15001, #15017, #15267) - **Structured output** β Thinking params for structured output, Bedrock structured generation, and DeepSeek `generateObject` tool choice. (#15051, #15174, #15054) - **Cost** β Chat cost estimate support; preserve usage cost in custom streams. (#14876, #15218) --- ## π¬ Chat & User Experience - **Follow-up chips** β Extend follow-up chip suggestions to general chat with scene-specific model config. (#15101, #14797) - **Input drafts** β Persist unsent input drafts across tab switches and prevent repeated draft restore. (#14992, #15024) - **Command menu** β Order topic/message search by recency and promote inline type filters. (#15094, #14986) - **Zoom HUD** β Show a zoom-level HUD on Cmd +/β and Cmd 0. (#15294) - **Copy** β Unescape markdown escapes when copying user messages. (#15253) --- ## π₯οΈ Desktop - **App Nap fix** β Prevent App Nap from dropping the gateway WebSocket during display sleep. (#14994) - **File preview** β Preview `.cjs`/`.mjs`/no-extension files instead of binary fallback and expand `~` when opening local files. (#15168, #15284) - **Cross-platform settings** β Open settings via main-window navigation on Windows/Linux and restore the route after an update restart. (#15036, #14922) - **Token refresh** β Prevent frequent logout from token-refresh retries. (#14928) --- ## π Observability - **OTel GenAI** β Instrument Agent Runtime with OpenTelemetry GenAI semantic conventions. (#15123) - **Generation tracing** β Per-call `llm_generation_tracing` with a pre-allocated tracingId and recordFeedback router. (#15124, #15146) - **Error classification** β Persist `ERROR_CODE_SPECS` classification on operation errors. (#15273) --- ## ποΈ Database Migrations - **Batch migrations** β Topic usage stats, push tokens, `tasks.editor_data`, and document shares. (#15280) - **Tracing & eval tables** β Add `llm_generation_tracing` and agent eval experiment tables. (#15126) > Self-hosted operators should run the database migration (`pnpm db:migrate`, or restart with auto-migrate enabled) after upgrading. The changes are additive and backwards-compatible. --- ## π Security & Reliability - **Security:** Remove the `getPlaintextCred` tool to prevent plaintext credential exposure. (#14998) - **Security:** Prompt account selection for Google OAuth and add `prompt=consent` to the OIDC authorization URL to fix missing refresh tokens. (#15234, #15010) - **Reliability:** Preserve streamed content across a mid-stream cancel. (#15173) - **Reliability:** Bound the Redis command timeout and configure the Anthropic client timeout. (#15091, #15042) - **Reliability:** Prevent infinite recursion in the assistant chain. (#15288) --- ## π₯ Contributors Huge thanks to **15 contributors** who shipped **220 merged PRs** this cycle. @AnotiaWang Β· @sxjeru Β· @algojogacor Β· @hardy-one Β· @arvinxx Β· @Innei Β· @tjx666 Β· @lijian Β· @AmAzing129 Β· @rdmclin2 Β· @neko Β· @cy948 Β· @CanisMinor Β· @sudongyuer Β· @rivertwilight Plus @lobehubbot and renovate[bot] for maintenance. --- **Full Changelog**: v2.2.0...release/weekly-20260528
Summary
UIChatMessage[]snapshot tostep_startandagent_runtime_endevents so the client can use the pushed payload as Source of Truth instead of refetching from DB.publishAgentRuntimeEndmigrated to a params-object signature (additiveuiMessagesfield); coordinator resolves the snapshot through an optionaluiMessagesResolverhook before publishing terminal events.MessageService.queryMessagesexposes the same read path as themessage.getMessagestRPC lambda (withFileService.postProcessUrlURL resolution)..agents/skills/local-testing/scripts/agent-gateway/to verify SoT regressions in-browser.Why
Gateway-mode streaming let the client refetch from DB on
step_complete+ tab-focus. Stream chunks land before the DB fan-out completes, so the refetch returned a stale assistant placeholder that clobbered the in-memory streamedassistantGroup(reasoning / tool calls / content). Probe trace fromLOBE-9501:step_startfires after the previous step's DB writes are awaited durable, so the snapshot query reflects strongly-consistent state β that's the contract that lets the client trust the pusheduiMessages.Compatibility
uiMessagesfield,finalStatepayload unchanged.uiMessagesso streaming never fails the step.agentNotifyandaiAgentmigrated to the params shape.Test plan
bunx vitest run src/server/modules/AgentRuntime/__tests__/ src/server/services/agentRuntime/__tests__/ src/server/services/agentRuntime/AgentRuntimeService.test.ts(391 passed)bun run type-check(clean β pre-existingcommandererrors inpackages/llm-generation-tracingare unrelated).agents/skills/local-testing/scripts/agent-gateway/βREGRESSIONSsection should no longer showchildN/cT/rTdrops on same topicrunAgent.tsfromrefreshMessages()toreplaceMessages(event.data.uiMessages)and gaterevalidateOnFocusduring streamingLinear: LOBE-9501
π€ Generated with Claude Code