fix: prevent duplicate AI design stream reconnects
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# Task: Fix AI design rapid session switching freeze
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260819-makelore-session-freeze-7c4e
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260818-prompt-museum-client-4f7a-prompt-museum-client
|
||||
- Worktree: D:\mk-4f7a
|
||||
- Base commit: f8d82e6c19536bbd9bd7d70a72efbf7ec163aeb4
|
||||
- Owner: codex
|
||||
- Status: Ready for integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Diagnose and fix the Makelore AI Design client freeze reported when a user
|
||||
rapidly switches Conversations after login.
|
||||
- Keep the change limited to the Renderer task-stream lifecycle and its unit
|
||||
regression coverage; do not change the Works API or persistent Agent Session
|
||||
contract.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Preserve the existing Workspace + Conversation identity and generation
|
||||
guards, including stale-stream isolation and REST reconciliation after the
|
||||
Canvas effect is cleaned up.
|
||||
- Do not issue a second event-stream open for the same active Workspace +
|
||||
Conversation solely because reconciliation was requested while its stream
|
||||
is `connecting`. An explicit Conversation selection may invalidate the old
|
||||
identity and start the newly selected Conversation's stream. Only a known
|
||||
`degraded` stream may be forced closed and reopened by reconciliation.
|
||||
- Use a regression test that models an unresolved `openImageWorkspaceTaskEvents`
|
||||
Promise, because the underlying connection attempt is not cancellable once
|
||||
started.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Confirmed the freeze mechanism: Canvas effect cleanup marks task-stream
|
||||
reconciliation while the stream is still `connecting`; the next connect
|
||||
call treated every non-`connected` state as broken, closed the logical
|
||||
stream, and started another request. The old Promise could not be cancelled
|
||||
and only closed its EventSource after eventually resolving, so rapid
|
||||
switching accumulated concurrent connection attempts.
|
||||
- Changed `connectTaskStream` to force a close/reconnect only for `degraded`
|
||||
streams. An in-flight `connecting` stream is reused while Conversation and
|
||||
task REST reconciliation still runs.
|
||||
- Added a store regression test proving reconciliation does not issue a second
|
||||
stream-open request while the first one is unresolved.
|
||||
- Audited the adjacent Main SSE relay close timing; the existing `finally`
|
||||
path closes a subscription after a response is already closed, and no
|
||||
reliable red test proved an additional leak in this task, so no Main/API
|
||||
change was made.
|
||||
|
||||
## Verification
|
||||
|
||||
- New regression before the fix: failed as expected (`openImageWorkspaceTaskEvents`
|
||||
was called twice instead of once).
|
||||
- `node_modules\\.bin\\vitest.cmd run tests/unit/image-workspace-store.test.ts
|
||||
--reporter=dot` — passed, 1 file / 31 tests.
|
||||
- `node_modules\\.bin\\vitest.cmd run tests/unit/image-canvas-page.test.tsx
|
||||
tests/unit/works-square-design-workspace.test.ts --reporter=dot` — passed,
|
||||
2 files / 78 tests. Existing React `act(...)` warnings were emitted by an
|
||||
unrelated auth test.
|
||||
- `node_modules\\.bin\\eslint.cmd src/stores/image-workspace.ts
|
||||
tests/unit/image-workspace-store.test.ts` — passed.
|
||||
- `node_modules\\.bin\\tsc.cmd --noEmit` — passed.
|
||||
- `git diff --check` — passed.
|
||||
- `check_doc_drift.py --task-id 20260819-makelore-session-freeze-7c4e` — passed.
|
||||
- Independent Sol read-only final review — PASS. The review noted the
|
||||
untested extreme A→B→A case where a deliberately stalled old open request
|
||||
may later be replaced; that is retained as a separate follow-up rather than
|
||||
part of the same-identity reconciliation race.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Run a real logged-in Electron smoke test that alternates two or more
|
||||
Conversations while the event-stream handshake is deliberately slow; local
|
||||
unit tests prove the duplicate-open race but do not validate a deployed Works
|
||||
gateway or production network behavior.
|
||||
- If production telemetry later shows a WebSocket handshake that never
|
||||
resolves, track a separate cancellation/timeout task instead of coupling it
|
||||
to this Renderer race fix.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None. The fix restores the existing client stream-lifecycle invariant and
|
||||
does not introduce a new architectural decision or canonical contract.
|
||||
@@ -1026,7 +1026,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
const conversationId = get().activeConversationId;
|
||||
const shouldReconcile = reconcileTaskStateOnNextConnect;
|
||||
reconcileTaskStateOnNextConnect = false;
|
||||
if (shouldReconcile && get().taskStreamState !== 'connected') {
|
||||
if (shouldReconcile && get().taskStreamState === 'degraded') {
|
||||
closeTaskEventSource();
|
||||
}
|
||||
if (workspaceId && conversationId) startTaskStream(workspaceId, conversationId);
|
||||
|
||||
@@ -385,6 +385,20 @@ describe('AI design task event store', () => {
|
||||
.toEqual(['继续优化海报', '已完成最新方向整理。']);
|
||||
});
|
||||
|
||||
it('keeps the in-flight stream connection when reconciliation is requested', async () => {
|
||||
const pendingSource = deferred<EventSource>();
|
||||
openImageWorkspaceTaskEventsMock.mockReturnValue(pendingSource.promise);
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
expect(useImageWorkspaceStore.getState().taskStreamState).toBe('connecting');
|
||||
expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce();
|
||||
|
||||
useImageWorkspaceStore.getState().markTaskStreamNeedsReconciliation();
|
||||
useImageWorkspaceStore.getState().connectTaskStream();
|
||||
|
||||
expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('recovers a committed generation task when the Agent Run fails afterward', async () => {
|
||||
const source = new MockEventSource();
|
||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||
|
||||
Reference in New Issue
Block a user