fix(pi): keep active runs alive during background sleep

This commit is contained in:
2026-08-25 20:56:17 +08:00
parent 92f4c91088
commit 621ebb1781
22 changed files with 1061 additions and 68 deletions

View File

@@ -215,6 +215,26 @@ async function waitForResilienceProof(electronApplication, predicate, message) {
throw new Error(`${message}: ${JSON.stringify(latest?.resilience)}`);
}
async function waitForResilienceIdleProof(electronApplication, predicate, message) {
const deadline = Date.now() + 30_000;
let latest;
while (Date.now() < deadline) {
latest = await evaluateProof(electronApplication, 'resilience.idle-status');
if (predicate(latest?.resilience)) return latest;
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
}
throw new Error(`${message}: ${JSON.stringify(latest?.resilience)}`);
}
async function setMainWindowVisible(electronApplication, visible) {
await electronApplication.evaluate(({ BrowserWindow }, shouldShow) => {
const window = BrowserWindow.getAllWindows()[0];
if (!window) throw new Error('Packaged Main window is unavailable');
if (shouldShow) window.show();
else window.hide();
}, visible);
}
function assertResilienceTerminal(
status,
expectedRunStatus = 'error',
@@ -543,7 +563,7 @@ export async function runPackagedProductProof(options) {
await resilienceTarget.click();
const resilienceComposer = page.getByRole('textbox');
await resilienceComposer.waitFor({ state: 'visible', timeout: 30_000 });
await resilienceComposer.fill('RESILIENCE_TARGET_ACTIVE');
await resilienceComposer.fill('BACKGROUND_ACTIVE');
try {
await page.getByRole('button', { name: '发送' }).click({ timeout: 30_000 });
} catch (error) {
@@ -557,6 +577,109 @@ export async function runPackagedProductProof(options) {
);
}
const backgroundActive = await waitForResilienceProof(
electronApplication,
(status) => status?.target?.runStatus === 'running'
&& status?.other?.runStatus === 'running'
&& status?.activeProviderRequests?.parent === 2
&& status?.resources?.backgroundLeases?.active === 2
&& status?.processes?.parent?.length === 2,
'Packaged background lease proof did not reach two active Main-owned runs',
);
await setMainWindowVisible(electronApplication, false);
await new Promise((resolveWait) => setTimeout(resolveWait, 600));
const backgroundHidden = await evaluateProof(electronApplication, 'resilience.status');
if (backgroundHidden?.resilience?.target?.runStatus !== 'running'
|| backgroundHidden?.resilience?.other?.runStatus !== 'running'
|| backgroundHidden?.resilience?.activeProviderRequests?.parent !== 2
|| backgroundHidden?.resilience?.resources?.backgroundLeases?.active !== 2
|| backgroundHidden?.resilience?.processes?.parent?.length !== 2) {
throw new Error(`Hidden packaged Pi run was stopped despite active leases: ${JSON.stringify(backgroundHidden?.resilience)}`);
}
await evaluateProof(electronApplication, 'resilience.release-parents');
await waitForResilienceProof(
electronApplication,
(status) => status?.target?.runStatus === 'idle'
&& status?.other?.runStatus === 'idle'
&& status?.resources?.backgroundLeases?.active === 0,
'Hidden packaged Pi runs did not settle and release their background leases',
);
await setMainWindowVisible(electronApplication, true);
await page.getByText('RESILIENCE_ACTIVE').last().waitFor({ state: 'visible', timeout: 30_000 });
if (!await resilienceComposer.isEnabled()) {
throw new Error('Packaged composer stayed disabled after the hidden run settled');
}
await setMainWindowVisible(electronApplication, false);
const backgroundIdle = await waitForResilienceIdleProof(
electronApplication,
(status) => status?.workers?.length === 0
&& status?.resources?.backgroundLeases?.active === 0
&& status?.resources?.pool?.processBudget?.active === 0
&& status?.processes?.parent?.length === 0
&& status?.processes?.child?.length === 0
&& status?.backgroundSleepReasoned === true,
'Settled hidden packaged Pi workers were not reasoned background-sleep evicted',
);
await setMainWindowVisible(electronApplication, true);
await page.reload();
await page.waitForLoadState('domcontentloaded');
if (await page.getByTestId('ai-module-selection-page').count()) {
await page.getByTestId('ai-module-option-programming').click();
await page.getByTestId('main-layout').waitFor({ state: 'visible', timeout: 10_000 });
}
await page.evaluate(() => { window.location.hash = '/chat'; });
await resilienceTarget.waitFor({ state: 'visible', timeout: 30_000 });
await resilienceTarget.click();
await resilienceComposer.waitFor({ state: 'visible', timeout: 30_000 });
const restartedOther = await evaluateProof(electronApplication, 'resilience.restart-other');
assertPackagedMain(restartedOther);
if (restartedOther?.resilience?.other?.runStatus !== 'running'
|| restartedOther?.resilience?.resources?.backgroundLeases?.active !== 1) {
throw new Error(`Packaged resilience sibling did not restart: ${JSON.stringify(restartedOther?.resilience)}`);
}
await resilienceComposer.fill('RESILIENCE_TARGET_ACTIVE');
await page.getByRole('button', { name: '发送' }).click({ timeout: 30_000 });
const intentionalActive = await waitForResilienceProof(
electronApplication,
(status) => status?.target?.runStatus === 'running'
&& status?.other?.runStatus === 'running'
&& status?.resources?.subagents?.activeChildPermits === 1
&& status?.resources?.subagents?.activeDispatches === 1
&& status?.resources?.extension?.registrations?.child === 1
&& status?.resources?.extension?.writeLeases?.active === 1
&& status?.resources?.backgroundLeases?.active === 2
&& status?.processes?.parent?.length === 2
&& status?.processes?.child?.length === 1,
'Packaged intentional-dispose parent/child state did not become active',
);
const intentionalDispose = await evaluateProof(electronApplication, 'resilience.dispose-target');
assertPackagedMain(intentionalDispose);
if (intentionalDispose?.resilience?.terminal?.observed !== true
|| intentionalDispose?.resilience?.terminal?.errorCode !== 'CODING_RUNTIME_START_FAILED'
|| intentionalDispose?.resilience?.terminal?.recoverable !== true
|| intentionalDispose?.resilience?.other?.runStatus !== 'running'
|| intentionalDispose?.resilience?.other?.bindingPreserved !== true
|| intentionalDispose?.resilience?.targetBindingPreserved !== true
|| intentionalDispose?.resilience?.providerRequestsUnchanged !== true
|| intentionalDispose?.resilience?.resources?.backgroundLeases?.active !== 1) {
throw new Error(`Packaged intentional dispose did not converge safely: ${JSON.stringify(intentionalDispose?.resilience)}`);
}
await page.getByText('本地 Agent 已中断,原请求未自动重发。').waitFor({ state: 'visible', timeout: 10_000 });
if (!await resilienceComposer.isEnabled()) {
throw new Error('Packaged composer stayed disabled after intentional dispose');
}
const recoveredAfterDispose = await recoverSelectedConversation(page, electronApplication);
if (JSON.stringify(recoveredAfterDispose.resilience.providerRequests)
!== JSON.stringify(intentionalActive.resilience.providerRequests)) {
throw new Error('Recover replayed the uncertain prompt after intentional dispose');
}
await resilienceComposer.fill('RESILIENCE_RECOVERED_TURN');
await page.getByRole('button', { name: '发送' }).click({ timeout: 30_000 });
await page.getByText('RESILIENCE_RECOVERED_COMPLETE').waitFor({ state: 'visible', timeout: 30_000 });
await resilienceComposer.fill('RESILIENCE_TARGET_ACTIVE');
await page.getByRole('button', { name: '发送' }).click({ timeout: 30_000 });
const activeResilience = await waitForResilienceProof(
electronApplication,
(status) => status?.target?.runStatus === 'running'
@@ -565,6 +688,7 @@ export async function runPackagedProductProof(options) {
&& status?.resources?.subagents?.activeDispatches === 1
&& status?.resources?.extension?.registrations?.child === 1
&& status?.resources?.extension?.writeLeases?.active === 1
&& status?.resources?.backgroundLeases?.active === 2
&& status?.processes?.parent?.length === 2
&& status?.processes?.child?.length === 1,
'Packaged resilience parent/child/write-lease fault state did not become active',
@@ -721,6 +845,21 @@ export async function runPackagedProductProof(options) {
},
resilience: {
setup: resilienceStart.resilience,
backgroundLifecycle: {
active: backgroundActive.resilience,
hiddenPastGrace: backgroundHidden.resilience,
idleEvicted: backgroundIdle.resilience,
uiPromptAccepted: true,
answerVisibleAfterShow: true,
composerUnlocked: true,
},
intentionalDispose: {
active: intentionalActive.resilience,
disposed: intentionalDispose.resilience,
recovered: recoveredAfterDispose.resilience,
siblingContinued: true,
uncertainPromptReplayed: false,
},
active: activeResilience.resilience,
exit: exitFailure.resilience,
abortAfterExit: abortAfterExit.resilience,