fix(coding): bound missing Pi settlement

This commit is contained in:
inman
2026-09-01 12:40:08 +08:00
parent 8062b45103
commit f1fd13a70f
6 changed files with 478 additions and 52 deletions

View File

@@ -58,6 +58,12 @@ class FakeWorker implements PiConversationWorker {
private readonly invalidationListeners = new Set<(error: PiProcessError) => void>();
private timeoutType: string | null = null;
private pendingType: string | null = null;
private stateData: unknown = {
isStreaming: true,
isCompacting: false,
pendingMessageCount: 0,
retryAttempt: 0,
};
private lateResult: ((result: {
response?: PiRpcResponse;
error?: PiProcessError;
@@ -86,7 +92,18 @@ class FakeWorker implements PiConversationWorker {
} | undefined)?.onLateResult;
throw new PiProcessFailure('PI_RPC_TIMEOUT', `fake ${command.type} confirmation timeout`);
}
return { type: 'response' as const, id: 'fake', success: true };
return {
type: 'response' as const,
id: 'fake',
success: true,
...(command.type === 'get_state'
? { data: structuredClone(this.stateData) as T }
: {}),
};
}
setState(state: unknown): void {
this.stateData = structuredClone(state);
}
timeoutNext(type: string): void {
@@ -310,6 +327,125 @@ describe('Pi worker pool', () => {
expect(pool.getResilienceProofDiagnostics().runs).toEqual({ active: 0, waiting: 0 });
});
it('settles from authoritative idle state when agent_settled is missing and ignores a late duplicate', async () => {
const worker = new FakeWorker('worker-target');
const events: PiWorkerPoolEvent[] = [];
const pool = new PiWorkerPool({
maxIdle: 2,
settlementProbeIntervalMs: 5,
settlementProbeTimeoutMs: 20,
terminalSettlementTimeoutMs: 100,
openWorker: async () => ({
worker,
session: { piSessionId: 'session-target', sessionKey: 'key-target' },
}),
});
pool.subscribe((event) => events.push(event));
await pool.prepare(conversation('conversation-target'));
const ticket = pool.startTopLevel({
conversationId: 'conversation-target',
runId: 'run-missing-settled',
command: { type: 'prompt', message: 'finish without settlement event' },
});
await expect(ticket.accepted).resolves.toMatchObject({ success: true });
worker.setState({
isStreaming: false,
isCompacting: false,
pendingMessageCount: 0,
retryAttempt: 0,
});
await expect.poll(() => pool.getActiveRun('conversation-target')).toBeNull();
expect(pool.getState('conversation-target')?.state).toBe('idle');
expect(events.filter((event) => event.type === 'top-level.settled')).toEqual([
expect.objectContaining({ source: 'state_probe' }),
]);
worker.emit({ type: 'agent_settled' });
expect(events.filter((event) => event.type === 'top-level.settled')).toHaveLength(1);
await pool.shutdown();
});
it('fails only the target thread when an accepted prompt rejects during settlement', async () => {
const workers = new Map<string, FakeWorker>();
const events: PiWorkerPoolEvent[] = [];
const pool = new PiWorkerPool({
maxIdle: 2,
openWorker: async ({ conversation: input }) => {
const worker = new FakeWorker(`worker-${input.conversationId}`);
workers.set(input.conversationId, worker);
return {
worker,
session: {
piSessionId: `session-${input.conversationId}`,
sessionKey: `key-${input.conversationId}`,
},
};
},
});
pool.subscribe((event) => events.push(event));
await Promise.all([
pool.prepare(conversation('conversation-target')),
pool.prepare(conversation('conversation-sibling')),
]);
const ticket = pool.startTopLevel({
conversationId: 'conversation-target',
runId: 'run-post-accept-failure',
command: { type: 'prompt', message: 'accepted then failed' },
});
await expect(ticket.accepted).resolves.toMatchObject({ success: true });
workers.get('conversation-target')!.emit({
type: 'makelore_thread_error',
code: 'PROMPT_FAILED_AFTER_ACCEPTANCE',
});
expect(pool.getActiveRun('conversation-target')).toBeNull();
expect(pool.getState('conversation-target')).toMatchObject({
state: 'crashed',
failureCode: 'PI_RPC_PROTOCOL_ERROR',
});
expect(pool.getState('conversation-sibling')).toMatchObject({ state: 'ready' });
expect(events).toContainEqual(expect.objectContaining({
type: 'worker.crashed',
conversationId: 'conversation-target',
}));
expect(workers.get('conversation-target')!.requests.filter(({ type }) => type === 'prompt'))
.toHaveLength(1);
await pool.shutdown();
});
it('bounds a contradictory terminal phase without replaying the accepted prompt', async () => {
const worker = new FakeWorker('worker-target');
const pool = new PiWorkerPool({
maxIdle: 2,
settlementProbeIntervalMs: 5,
settlementProbeTimeoutMs: 20,
terminalSettlementTimeoutMs: 25,
openWorker: async () => ({
worker,
session: { piSessionId: 'session-target', sessionKey: 'key-target' },
}),
});
await pool.prepare(conversation('conversation-target'));
const ticket = pool.startTopLevel({
conversationId: 'conversation-target',
runId: 'run-terminal-stall',
command: { type: 'prompt', message: 'terminal stall' },
});
await expect(ticket.accepted).resolves.toMatchObject({ success: true });
worker.emit({
type: 'message_end',
message: { role: 'assistant', content: [], stopReason: 'stop' },
});
await expect.poll(() => pool.getState('conversation-target')?.state).toBe('crashed');
expect(pool.getActiveRun('conversation-target')).toBeNull();
expect(worker.requests.filter(({ type }) => type === 'prompt')).toHaveLength(1);
await pool.shutdown();
});
it('injects a proof failure only into the requested current generation', async () => {
const workers = new Map<string, FakeWorker>();
const pool = new PiWorkerPool({