Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/desktop/e2e/quote-companion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ test('side chat recovers from failures and preserves its draft', async ({
failPanel.getByText(/Fake backend received: retry after deterministic network failure/),
).toBeVisible();

await failComposer.fill('__e2e_error__:auth');
await failComposer.fill('__e2e_wait_for_steering__');
await failComposer.press('Enter');
const activeSideTab = page.locator(
'.maka-workbar-tab[data-running][data-workbar-tab-id^="side-chat:"]',
Expand All @@ -722,7 +722,6 @@ test('side chat recovers from failures and preserves its draft', async ({
await expect
.poll(async () => (await page.evaluate(() => window.maka.sessions.list())).length)
.toBe(1);
await expect(page.getByText('鉴权失败')).toHaveCount(0);

// Draft survival across collapse and launcher navigation.
await waitForSourceSessionToSettle(page);
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/__tests__/quote-companion-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
applyCompanionInteractionEvent,
cleanupCompanionCopy,
createCompanionDismissalGuard,
dismissCompanionCopy,
companionRunEventEffect,
deriveCompanionComposerState,
isCompanionTurnTerminal,
Expand Down Expand Up @@ -58,6 +59,7 @@ interface FakeControl {

function makeApi(control: FakeControl = {}) {
const calls = {
lifecycle: [] as string[],
removed: [] as string[],
sent: [] as {
id: string;
Expand Down Expand Up @@ -95,13 +97,17 @@ function makeApi(control: FakeControl = {}) {
return forked;
},
cleanupSessionCopy: async (id) => {
calls.lifecycle.push(`cleanup:${id}`);
calls.removed.push(id);
if (control.cleanupThrows) throw new Error('cleanup failed');
},
abandonSessionCopy: async (id) => {
calls.abandoned.push(id);
if (control.abandonThrows) throw new Error('abandon acknowledgement lost');
},
stop: async (id) => {
calls.lifecycle.push(`stop:${id}`);
},
send: async (id, cmd) => {
calls.sent.push({ id, cmd });
if (control.sendThrows) throw new Error('send failed');
Expand Down Expand Up @@ -172,6 +178,19 @@ describe('companion dismissal guard', () => {
assert.equal(replayedCleanup(), false);
assert.equal(activeCleanup(), true);
});

it('stops a live companion before dismissing its durable copy', async () => {
const { api, calls } = makeApi();

assert.equal(
await dismissCompanionCopy(api, 'source-session', 'panel-1', 'companion-session'),
true,
);
assert.deepEqual(calls.lifecycle, [
'stop:companion-session',
'cleanup:companion-session',
]);
});
});

describe('deriveCompanionComposerState', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,35 @@ test('merges a configuration patch into each fresh CAS projection', async () =>
);
});

test('retries a Session update through transient revision churn', async () => {
const responses: unknown[] = [];
for (let revision = 10; revision < 14; revision += 1) {
responses.push(
{ kind: 'session', session: session('session-1', revision) },
{
kind: 'revision_conflict',
expectedRevision: revision,
actualRevision: revision + 1,
},
);
}
responses.push(
{ kind: 'session', session: session('session-1', 14) },
{
kind: 'committed',
session: session('session-1', 15, { collaborationMode: 'plan' }),
},
);
const { client } = clientWithResponses(responses);

const updated = await client.updateSessionConfiguration('session-1', {
collaborationMode: 'plan',
});

assert.equal(updated.revision, 15);
assert.equal(updated.collaborationMode, 'plan');
});

test('rebuilds a Runtime Policy mutation from each fresh CAS projection', async () => {
const initial = createDefaultRuntimePolicy();
const concurrent = {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
} from "@maka/runtime-host/protocol";

const MAX_OPTIMISTIC_ATTEMPTS = 3;
const MAX_SESSION_REVISION_ATTEMPTS = 8;
const MAX_PRICING_SNAPSHOT_ATTEMPTS = 3;

export type DesktopSessionConfigurationPatch = Partial<SessionConfiguration>;
Expand Down Expand Up @@ -730,7 +731,7 @@ export class DesktopRuntimeHostClient {
}

async removeSession(sessionId: string): Promise<void> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
for (let attempt = 0; attempt < MAX_SESSION_REVISION_ATTEMPTS; attempt += 1) {
const current = await this.#requireSession(sessionId);
const result = await this.#request("session.remove", {
sessionId,
Expand Down Expand Up @@ -1379,7 +1380,7 @@ export class DesktopRuntimeHostClient {
sessionId: string,
update: (current: SessionCatalogProjection) => Promise<SessionUpdateResult>,
): Promise<SessionCatalogProjection> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
for (let attempt = 0; attempt < MAX_SESSION_REVISION_ATTEMPTS; attempt += 1) {
const current = await this.#requireSession(sessionId);
const result = await update(current);
if (result.kind === "committed")
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/renderer/quote-companion-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface CompanionSessionApi {
): Promise<SessionSummary>;
cleanupSessionCopy(sessionId: string): Promise<void>;
abandonSessionCopy(sessionId: string): Promise<void>;
stop(sessionId: string): Promise<void>;
send(
sessionId: string,
command: {
Expand Down Expand Up @@ -170,6 +171,16 @@ export async function cleanupCompanionCopy(
}
}

export async function dismissCompanionCopy(
api: CompanionSessionApi,
sourceSessionId: string,
panelId: string,
companionSessionId: string,
): Promise<boolean> {
await api.stop(companionSessionId).catch(() => undefined);
return cleanupCompanionCopy(api, sourceSessionId, panelId, companionSessionId);
}

/**
* The shared Composer's `streaming` input means "a turn is interruptible", not
* merely "a text delta has arrived". Keep the companion interruptible from the
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/use-quote-companion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import type { RendererIngestInput } from '../preload/bridge-contract.js';
import {
abandonPendingCompanionCopy,
applyCompanionInteractionEvent,
cleanupCompanionCopy,
createCompanionDismissalGuard,
dismissCompanionCopy,
companionRunEventEffect,
deriveCompanionComposerState,
ensureCompanionFork,
Expand Down Expand Up @@ -307,7 +307,7 @@ export function useQuoteCompanion(input: UseQuoteCompanionInput): UseQuoteCompan
const sourceSessionId = sourceSessionIdRef.current;
const id = companionIdRef.current ?? pendingForkIdRef.current;
if (id && sourceSessionId) {
void cleanupCompanionCopy(
void dismissCompanionCopy(
window.maka.sessions,
sourceSessionId,
panelId,
Expand Down
Loading