fix: resolve workspace teardown race with persistence fence
This commit is contained in:
parent
a69022756a
commit
dba6a88a28
5 changed files with 39 additions and 10 deletions
|
|
@ -191,6 +191,7 @@ import {
|
|||
stopAgentDispatcher,
|
||||
isSidecarAlive,
|
||||
setSidecarAlive,
|
||||
waitForPendingPersistence,
|
||||
} from './agent-dispatcher';
|
||||
|
||||
// Stop any previous dispatcher between tests so `unlistenMsg` is null and start works
|
||||
|
|
@ -601,4 +602,11 @@ describe('agent-dispatcher', () => {
|
|||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForPendingPersistence', () => {
|
||||
it('resolves immediately when no persistence is in-flight', async () => {
|
||||
vi.useRealTimers();
|
||||
await expect(waitForPendingPersistence()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ let unlistenExit: (() => void) | null = null;
|
|||
// Map sessionId -> projectId for persistence routing
|
||||
const sessionProjectMap = new Map<string, string>();
|
||||
|
||||
// In-flight persistence counter — prevents teardown from racing with async saves
|
||||
let pendingPersistCount = 0;
|
||||
|
||||
export function registerSessionProject(sessionId: string, projectId: string): void {
|
||||
sessionProjectMap.set(sessionId, projectId);
|
||||
}
|
||||
|
|
@ -273,6 +276,13 @@ function spawnSubagentPane(parentSessionId: string, tc: ToolCallContent): void {
|
|||
}
|
||||
}
|
||||
|
||||
/** Wait until all in-flight persistence operations complete */
|
||||
export async function waitForPendingPersistence(): Promise<void> {
|
||||
while (pendingPersistCount > 0) {
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist session state + messages to SQLite for the project that owns this session */
|
||||
async function persistSessionForProject(sessionId: string): Promise<void> {
|
||||
const projectId = sessionProjectMap.get(sessionId);
|
||||
|
|
@ -281,6 +291,7 @@ async function persistSessionForProject(sessionId: string): Promise<void> {
|
|||
const session = getAgentSession(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
pendingPersistCount++;
|
||||
try {
|
||||
// Save agent state
|
||||
await saveProjectAgentState({
|
||||
|
|
@ -313,6 +324,8 @@ async function persistSessionForProject(sessionId: string): Promise<void> {
|
|||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to persist agent session:', e);
|
||||
} finally {
|
||||
pendingPersistCount--;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@
|
|||
<div class="group-list">
|
||||
{#each groups as group}
|
||||
<div class="group-row" class:active={group.id === activeGroupId}>
|
||||
<button class="group-name" onclick={() => switchGroup(group.id)}>
|
||||
<button class="group-name" onclick={async () => await switchGroup(group.id)}>
|
||||
{group.name}
|
||||
</button>
|
||||
<span class="group-count">{group.projects.length} projects</span>
|
||||
|
|
@ -764,14 +764,14 @@
|
|||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.1875rem;
|
||||
border: 1px solid;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-colors {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
gap: 0.1875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -786,13 +786,13 @@
|
|||
.size-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.size-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -814,7 +814,7 @@
|
|||
}
|
||||
|
||||
.size-input {
|
||||
width: 40px;
|
||||
width: 2.5rem;
|
||||
padding: 0.25rem 0.125rem;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
|
|
@ -833,7 +833,7 @@
|
|||
.size-unit {
|
||||
font-size: 0.7rem;
|
||||
color: var(--ctp-overlay0);
|
||||
margin-right: 2px;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
/* Groups */
|
||||
|
|
@ -959,8 +959,8 @@
|
|||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
background: var(--ctp-text);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { loadGroups, saveGroups, getCliGroup } from '../adapters/groups-bridge';
|
||||
import type { GroupsFile, GroupConfig, ProjectConfig } from '../types/groups';
|
||||
import { clearAllAgentSessions } from '../stores/agents.svelte';
|
||||
import { waitForPendingPersistence } from '../agent-dispatcher';
|
||||
|
||||
export type WorkspaceTab = 'sessions' | 'docs' | 'context' | 'settings';
|
||||
|
||||
|
|
@ -69,6 +70,9 @@ export function setActiveProject(projectId: string | null): void {
|
|||
export async function switchGroup(groupId: string): Promise<void> {
|
||||
if (groupId === activeGroupId) return;
|
||||
|
||||
// Wait for any in-flight persistence before clearing state
|
||||
await waitForPendingPersistence();
|
||||
|
||||
// Teardown: clear terminal tabs and agent sessions for the old group
|
||||
projectTerminals = {};
|
||||
clearAllAgentSessions();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ vi.mock('../stores/agents.svelte', () => ({
|
|||
clearAllAgentSessions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../agent-dispatcher', () => ({
|
||||
waitForPendingPersistence: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../adapters/groups-bridge', () => ({
|
||||
loadGroups: vi.fn().mockImplementation(() => Promise.resolve(mockGroupsData())),
|
||||
saveGroups: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue