Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c37a3f
fix(desktop,runtime): interrupt and bound empty assistant loops
Rangsh Sep 4, 2026
270774f
fix(desktop): keep mid-turn interrupt off AppShell bridge debt
Rangsh Sep 4, 2026
70f29b2
fix(runtime): format empty-loop regression tests for biome
Rangsh Sep 5, 2026
d5e9565
fix(ui): allow onStop to return boolean for mid-turn interrupt
Rangsh Sep 5, 2026
7b57693
test(runtime): use distinct tool paths in overflow recovery growth case
Rangsh Sep 5, 2026
2ae395c
fix(desktop,runtime): tighten empty-step loop bound from review
Rangsh Sep 11, 2026
4743ecb
fix(desktop,runtime): reset empty-step streak on steer and pin interr…
Rangsh Sep 27, 2026
9e21e19
merge: sync empty-ai-reply-loop branch with apache/maka main
Rangsh Sep 27, 2026
f4e0fbd
fix(desktop): keep interrupt-before-send under AppShell debt ratchet
Rangsh Sep 27, 2026
8d19b27
merge: sync empty-ai-reply-loop branch with apache/maka main
Rangsh Sep 28, 2026
ac4e116
chore(docs): refresh Astryx surface inventory after main merge
Rangsh Sep 28, 2026
fe628b6
fix(ui): restore Archive/Unarchive icons dropped in main merge
Rangsh Sep 28, 2026
c631214
merge: sync empty-ai-reply-loop branch with apache/maka main
Rangsh Sep 28, 2026
66d3615
merge: sync empty-ai-reply-loop branch with current main
Rangsh Sep 28, 2026
0bd9c71
fix(desktop,runtime): align Enter help and CI with interrupt-on-send
Rangsh Sep 28, 2026
b72a5c4
fix(desktop): restore stop-path types and pin interrupt turn ownership
Rangsh Sep 29, 2026
8c600e0
fix(desktop): treat Host stop no-op as failed Enter interrupt
Rangsh Oct 2, 2026
4a7eca9
merge: sync empty-ai-reply-loop branch with apache/maka main
Rangsh Oct 2, 2026
b1f8095
fix(desktop): keep interrupt-before-send under architecture ratchet
Rangsh Oct 2, 2026
dac89ea
merge: sync empty-ai-reply-loop branch with apache/maka main
Rangsh Oct 4, 2026
92a5d37
fix(desktop): drop exports orphaned by the composer-submission merge
Rangsh Oct 4, 2026
8a0ed25
fix(runtime): hash empty-step signatures incrementally
Rangsh Oct 4, 2026
0c0381e
fix(desktop): report stop outcomes so Enter interrupt toasts once
Rangsh Oct 4, 2026
2d8d531
chore(protocol): take epoch 206 to avoid colliding with #5709
Rangsh Oct 4, 2026
47e2a74
chore(protocol): word the epoch 205 reservation durably
Rangsh Oct 4, 2026
aa1b4b4
chore(protocol): drop redundant epoch 206 wording declaration
Rangsh Oct 4, 2026
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
39 changes: 17 additions & 22 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async function submitSteeringDraft(
await composer.press("ControlOrMeta+Enter");
}

test("ordinary Enter queues on an already-running Session before observation recovers", async ({
test("ordinary Enter interrupts an already-running Session before observation recovers", async ({
window: page,
}) => {
const nextPrompt = "do this only after the current answer";
Expand Down Expand Up @@ -107,39 +107,34 @@ test("ordinary Enter queues on an already-running Session before observation rec
const sidebar = page.getByRole("navigation", { name: "任务列表" });
await ensureSidebarExpanded(page);
await sessionRow(sidebar, sessionId).click();
// No execution snapshot has reached this surface. Sending must still express next-turn intent.
// No execution snapshot has reached this surface. Sending must still interrupt
// via Host-known running turns rather than queue a follow-up (#4083).
await expect(
page.getByRole("button", { name: "停止", exact: true }),
).toHaveCount(0);
const composer = page.locator(COMPOSER_INPUT);
await composer.fill(nextPrompt);
await awaitSendReady(page);
await composer.press("Enter");
await expect
.poll(() =>
page.evaluate(
() =>
(
window as typeof window & {
admissionEvidence?: { queued: boolean; steered: boolean };
}
).admissionEvidence,
),
)
.toEqual({ queued: true, steered: false });
// After interrupt, the typed draft is admitted as a new root turn — not queued.
await expect(page.getByRole("log")).toContainText(nextPrompt, {
timeout: 20_000,
});
expect(
await page.evaluate(
() =>
(
window as typeof window & {
admissionEvidence?: { queued: boolean; steered: boolean };
}
).admissionEvidence,
),
).toEqual({ queued: false, steered: false });
await page.evaluate(() =>
(window as SessionObservationLatchWindow).makaE2eLatch!.release(
"sessions.observe",
),
);
await expect(page.locator(".maka-bubble-streaming")).toContainText(
"Fake backend waiting",
{ timeout: 20_000 },
);
await page.getByRole("button", { name: "停止", exact: true }).click();
await expect(
page.getByRole("button", { name: "停止", exact: true }),
).toHaveCount(0, { timeout: 20_000 });
});

test("a failed transcript open recovers when its Session observation becomes ready", async ({
Expand Down
172 changes: 170 additions & 2 deletions apps/desktop/src/main/__tests__/app-shell-stop-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ test('removes exactly the transient messages the Host retracts while stopping',
toastApi: { error() {} },
});

await stop();

assert.equal(await stop(), 'interrupted');
assert.deepEqual(removed, [
{ sessionId: 'session-1', messageId: 'message-1' },
{ sessionId: 'session-1', messageId: 'message-2' },
Expand All @@ -56,3 +55,172 @@ test('removes exactly the transient messages the Host retracts while stopping',
target.window = previousWindow;
}
});

test('reports a thrown stop as failed after toasting it', async () => {
const target = globalThis as unknown as { window?: unknown };
const previousWindow = target.window;
const errors: string[] = [];
target.window = {
maka: {
sessions: {
stop: async () => {
throw new Error('stop failed');
},
},
},
};
try {
const stop = createStopAction({
services: windowSubmissionServices(),
uiLocale: 'en',
activeIdRef: { current: 'session-1' },
stopPending: { claim: () => true, release: () => undefined },
removeTransientMessage: () => undefined,
toastApi: {
error(title) {
errors.push(title);
},
},
});

assert.equal(await stop(), 'failed');
assert.equal(errors.length, 1);
} finally {
target.window = previousWindow;
}
});

test('reports a Host no-op stop as not_running when expectedTurnId is pinned', async () => {
const target = globalThis as unknown as { window?: unknown };
const previousWindow = target.window;
const stopped: Array<{ sessionId: string; options: unknown }> = [];
target.window = {
maka: {
sessions: {
stop: async (sessionId: string, options?: unknown) => {
stopped.push({ sessionId, options });
// Host returns undefined when expectedTurnId no longer matches the
// live root (settled or replaced by a newer turn).
return undefined;
},
},
},
};
try {
const stop = createStopAction({
services: windowSubmissionServices(),
uiLocale: 'en',
activeIdRef: { current: 'session-1' },
stopPending: { claim: () => true, release: () => undefined },
removeTransientMessage: () => undefined,
toastApi: { error() {} },
});

assert.equal(await stop('session-1', 'turn-a'), 'not_running');
assert.deepEqual(stopped, [
{
sessionId: 'session-1',
options: { source: 'stop_button', expectedTurnId: 'turn-a' },
},
]);
} finally {
target.window = previousWindow;
}
});

test('stops the captured Session when the active id changes during the await', async () => {
const target = globalThis as unknown as { window?: unknown };
const previousWindow = target.window;
const stopped: Array<{ sessionId: string; options: unknown }> = [];
const activeIdRef = { current: 'session-a' as string | undefined };
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
target.window = {
maka: {
sessions: {
stop: async (sessionId: string, options?: unknown) => {
stopped.push({ sessionId, options });
await gate;
return { kind: 'interrupted', retractedMessageIds: [] };
},
},
},
};
try {
const stop = createStopAction({
services: windowSubmissionServices(),
uiLocale: 'en',
activeIdRef,
stopPending: { claim: () => true, release: () => undefined },
removeTransientMessage: () => undefined,
toastApi: { error() {} },
});

const pending = stop('session-a', 'turn-1');
activeIdRef.current = 'session-b';
release();
assert.equal(await pending, 'interrupted');
assert.deepEqual(stopped, [
{ sessionId: 'session-a', options: { source: 'stop_button', expectedTurnId: 'turn-1' } },
]);
} finally {
target.window = previousWindow;
}
});

test('a second stop for the same Session awaits the one in flight', async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
let hostCalls = 0;
let held = false;
const stop = createStopAction({
services: {
stop: async () => {
hostCalls += 1;
await gate;
return { kind: 'interrupted', retractedMessageIds: [] };
},
},
uiLocale: 'en',
activeIdRef: { current: 'session-1' },
stopPending: {
claim: () => (held ? false : (held = true)),
release: () => {
held = false;
},
},
removeTransientMessage: () => undefined,
toastApi: { error() {} },
inFlight: new Map(),
});

const first = stop('session-1');
const second = stop('session-1', 'turn-1');
release();
assert.deepEqual(await Promise.all([first, second]), ['interrupted', 'interrupted']);
assert.equal(hostCalls, 1);
});

test('reports busy when another owner holds the stop claim', async () => {
let hostCalls = 0;
const stop = createStopAction({
services: {
stop: async () => {
hostCalls += 1;
return undefined;
},
},
uiLocale: 'en',
activeIdRef: { current: 'session-1' },
stopPending: { claim: () => false, release: () => undefined },
removeTransientMessage: () => undefined,
toastApi: { error() {} },
});

assert.equal(await stop('session-1'), 'busy');
assert.equal(hostCalls, 0);
});
Loading