merge: integrate AI design session freeze fix
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 conversationId = get().activeConversationId;
|
||||||
const shouldReconcile = reconcileTaskStateOnNextConnect;
|
const shouldReconcile = reconcileTaskStateOnNextConnect;
|
||||||
reconcileTaskStateOnNextConnect = false;
|
reconcileTaskStateOnNextConnect = false;
|
||||||
if (shouldReconcile && get().taskStreamState !== 'connected') {
|
if (shouldReconcile && get().taskStreamState === 'degraded') {
|
||||||
closeTaskEventSource();
|
closeTaskEventSource();
|
||||||
}
|
}
|
||||||
if (workspaceId && conversationId) startTaskStream(workspaceId, conversationId);
|
if (workspaceId && conversationId) startTaskStream(workspaceId, conversationId);
|
||||||
|
|||||||
@@ -385,6 +385,20 @@ describe('AI design task event store', () => {
|
|||||||
.toEqual(['继续优化海报', '已完成最新方向整理。']);
|
.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 () => {
|
it('recovers a committed generation task when the Agent Run fails afterward', async () => {
|
||||||
const source = new MockEventSource();
|
const source = new MockEventSource();
|
||||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||||
|
|||||||
Reference in New Issue
Block a user