merge: integrate agent server sleep restart
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Task: Diagnose local Agent received-message stall
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260901-agent-received-stall-8b6d4c21
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260901-agent-received-stall-8b6d4c21-agent-received-stall
|
||||
- Worktree: D:\Datas\OthersProjects\makelore-worktrees\agent-received-stall-8b6d4c21
|
||||
- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Diagnose the installed-app state where a prompt is acknowledged locally but remains optimistic indefinitely.
|
||||
- Fix the background-sleep versus Agent Server start race without replaying accepted prompts.
|
||||
- Add focused regression coverage for both lifecycle race windows.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Preserve the single shared Pi Agent Server and per-conversation logical thread architecture.
|
||||
- Background sleep must not stop work that begins while idle worker cleanup is in flight.
|
||||
- A start racing an already-started stop must wait for the stop and then create a fresh server process.
|
||||
- Do not auto-replay an accepted or uncertain prompt.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Confirmed installed-state evidence: the accepted UI state was newer than the latest Pi session write, and no Agent Server process remained live, so the prompt had not reached Provider generation.
|
||||
- Confirmed regression: `PiAgentServerProcess.start()` returned immediately when the old child still existed during an in-flight stop; after that stop completed, no server remained. The real-process race test failed before the fix and passed afterward.
|
||||
- Confirmed regression: composition checked for active work only before asynchronous background cleanup, then stopped the shared Agent Server even when a new run began during cleanup. The composition race test failed before the fix and passed afterward.
|
||||
- Fixed both lifecycle windows: background sleep rechecks active work after worker cleanup, and a start racing a stop waits for that stop before creating a fresh server.
|
||||
- The installed-app symptom maps directly to these two confirmed races, although the exact renderer/Main interleaving of the reported occurrence was not captured live.
|
||||
- Accepted and uncertain prompts are still never auto-replayed.
|
||||
|
||||
## Verification
|
||||
|
||||
- Installed-state red loop: accepted screenshot timestamp is newer than the latest Pi session write while no Agent Server process is live.
|
||||
- Red phase: the two focused race tests both failed against the original implementation.
|
||||
- `pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 2 files, 4 tests passed.
|
||||
- `pnpm exec vitest run tests/unit/background-lifecycle.test.ts tests/unit/pi-background-lifecycle.test.ts tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 4 files, 9 tests passed.
|
||||
- `pnpm run typecheck` — passed.
|
||||
- `pnpm run lint:check` — passed with 5 pre-existing warnings and no errors.
|
||||
- `pnpm test` — 216 files passed; 1,764 tests passed and 2 skipped.
|
||||
- `pnpm run build:vite` — Renderer, Main, Preload, and utility production builds passed.
|
||||
- Existing Electron E2E fixtures mock prompt acceptance and do not exercise the real Main-owned Agent Server lifecycle; the real-process regression test is the relevant product-path coverage.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Integrate the feature commit into `main`, then produce/reinstall a Windows package before validating the original installed-app reproduction; the currently installed binary does not contain this source fix.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None. The change enforces existing background-lease and single-Agent-Server architecture rather than changing canonical product behavior.
|
||||
@@ -498,6 +498,7 @@ export function createCodingComposition(
|
||||
await Promise.allSettled(conversationIds.map((conversationId) => (
|
||||
runtime.dispose(conversationId, reason)
|
||||
)));
|
||||
if (reason === 'background_sleep' && runtime.hasActiveWork()) return;
|
||||
await agentServer.stop();
|
||||
},
|
||||
async shutdown() {
|
||||
|
||||
@@ -490,6 +490,7 @@ export class PiAgentServerProcess {
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
if (this.stopFlight) return this.stopFlight.then(() => this.start());
|
||||
if (this.startFlight) return this.startFlight;
|
||||
if (this.child && !this.failure) return Promise.resolve();
|
||||
this.startFlight = this.startServer().finally(() => {
|
||||
|
||||
62
tests/unit/coding-composition-background-sleep.test.ts
Normal file
62
tests/unit/coding-composition-background-sleep.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { AgentBrowserModule } from '../../electron/agent-browser';
|
||||
import { createCodingComposition } from '../../electron/api/coding-composition';
|
||||
import { PiAgentServerProcess } from '../../electron/coding-runtime/pi/agent-server-process';
|
||||
import { createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store';
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('coding composition background sleep', () => {
|
||||
it('does not stop the shared Agent Server when work starts during worker cleanup', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-project-'));
|
||||
const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-user-'));
|
||||
roots.push(projectPath, userDataDir);
|
||||
const composition = createCodingComposition({
|
||||
storage: createMemoryCodingProjectStorage(),
|
||||
browser: { close: vi.fn(async () => undefined) } as unknown as AgentBrowserModule,
|
||||
paths: {
|
||||
executablePath: process.execPath,
|
||||
cliPath: path.join(projectPath, 'unused-cli.js'),
|
||||
serverPath: path.join(projectPath, 'unused-server.mjs'),
|
||||
userDataDir,
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
},
|
||||
});
|
||||
const stopAgentServer = vi.spyOn(PiAgentServerProcess.prototype, 'stop')
|
||||
.mockResolvedValue(undefined);
|
||||
let activeWork = false;
|
||||
const hasActiveWork = vi.spyOn(composition.runtime, 'hasActiveWork')
|
||||
.mockImplementation(() => activeWork);
|
||||
const getDiagnostics = vi.spyOn(composition.runtime, 'getDiagnostics')
|
||||
.mockReturnValue({
|
||||
workers: [{ conversationId: 'conversation-a' }],
|
||||
} as ReturnType<typeof composition.runtime.getDiagnostics>);
|
||||
const dispose = vi.spyOn(composition.runtime, 'dispose').mockImplementation(async () => {
|
||||
activeWork = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await composition.sleep('background_sleep');
|
||||
|
||||
expect(dispose).toHaveBeenCalledWith('conversation-a', 'background_sleep');
|
||||
expect(hasActiveWork).toHaveBeenCalledTimes(2);
|
||||
expect(stopAgentServer).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
dispose.mockRestore();
|
||||
getDiagnostics.mockRestore();
|
||||
hasActiveWork.mockRestore();
|
||||
stopAgentServer.mockRestore();
|
||||
await composition.shutdown();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -133,6 +133,30 @@ describe('Pi Agent Server real process', () => {
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
it('restarts when start races with a graceful background stop', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-restart-'));
|
||||
roots.push(root);
|
||||
const layout = await materializePackagedAgentServerLayout(root);
|
||||
const server = new PiAgentServerProcess({
|
||||
executablePath: electronExecutable,
|
||||
...layout,
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const firstProcessId = server.processId;
|
||||
expect(firstProcessId).toBeTypeOf('number');
|
||||
|
||||
const stopping = server.stop();
|
||||
const restarting = server.start();
|
||||
await Promise.all([stopping, restarting]);
|
||||
expect(server.processId).toBeTypeOf('number');
|
||||
expect(server.processId).not.toBe(firstProcessId);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
}, 20_000);
|
||||
|
||||
it('hosts isolated Conversation threads in one long-lived process', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-'));
|
||||
roots.push(root);
|
||||
|
||||
Reference in New Issue
Block a user