mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): support concurrent user bash cancellation (#7103)
This commit is contained in:
parent
c2275d6703
commit
2efa728d2e
2 changed files with 73 additions and 6 deletions
|
|
@ -334,7 +334,7 @@ export class AgentSession {
|
|||
private _retryAttempt = 0;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
private readonly _bashAbortControllers = new Set<AbortController>();
|
||||
private _pendingBashMessages: BashExecutionMessage[] = [];
|
||||
|
||||
// Extension system
|
||||
|
|
@ -2766,7 +2766,8 @@ export class AgentSession {
|
|||
onChunk?: (chunk: string) => void,
|
||||
options?: { excludeFromContext?: boolean; id?: string; operations?: BashOperations },
|
||||
): Promise<BashResult> {
|
||||
this._bashAbortController = new AbortController();
|
||||
const abortController = new AbortController();
|
||||
this._bashAbortControllers.add(abortController);
|
||||
|
||||
// Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
|
||||
const prefix = this.settingsManager.getShellCommandPrefix();
|
||||
|
|
@ -2783,14 +2784,14 @@ export class AgentSession {
|
|||
onChunk?.(delta);
|
||||
this._emit({ type: "bash_execution_update", id: options?.id, delta });
|
||||
},
|
||||
signal: this._bashAbortController.signal,
|
||||
signal: abortController.signal,
|
||||
},
|
||||
);
|
||||
|
||||
this.recordBashResult(command, result, options);
|
||||
return result;
|
||||
} finally {
|
||||
this._bashAbortController = undefined;
|
||||
this._bashAbortControllers.delete(abortController);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2828,12 +2829,14 @@ export class AgentSession {
|
|||
* Cancel running bash command.
|
||||
*/
|
||||
abortBash(): void {
|
||||
this._bashAbortController?.abort();
|
||||
for (const abortController of [...this._bashAbortControllers]) {
|
||||
abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a bash command is currently running */
|
||||
get isBashRunning(): boolean {
|
||||
return this._bashAbortController !== undefined;
|
||||
return this._bashAbortControllers.size > 0;
|
||||
}
|
||||
|
||||
/** Whether there are pending bash messages waiting to be flushed */
|
||||
|
|
|
|||
|
|
@ -10,6 +10,24 @@ function getEntryTypes(harness: Harness): string[] {
|
|||
return harness.sessionManager.getEntries().map((entry) => entry.type);
|
||||
}
|
||||
|
||||
interface ControlledBashInvocation {
|
||||
signal: AbortSignal | undefined;
|
||||
finish: () => void;
|
||||
}
|
||||
|
||||
function createControlledBashOperations(invocations: ControlledBashInvocation[]): BashOperations {
|
||||
return {
|
||||
exec: async (_command, _cwd, options) => {
|
||||
return await new Promise<{ exitCode: number | null }>((resolve) => {
|
||||
invocations.push({
|
||||
signal: options.signal,
|
||||
finish: () => resolve({ exitCode: 0 }),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("AgentSession bash and persistence characterization", () => {
|
||||
const harnesses: Harness[] = [];
|
||||
|
||||
|
|
@ -132,6 +150,52 @@ describe("AgentSession bash and persistence characterization", () => {
|
|||
expect(harness.session.isBashRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps newer bash execution tracked when an older execution finishes", async () => {
|
||||
const harness = await createHarness();
|
||||
harnesses.push(harness);
|
||||
const invocations: ControlledBashInvocation[] = [];
|
||||
const operations = createControlledBashOperations(invocations);
|
||||
|
||||
const firstBash = harness.session.executeBash("first", undefined, { operations });
|
||||
const secondBash = harness.session.executeBash("second", undefined, { operations });
|
||||
|
||||
invocations[0].finish();
|
||||
const firstResult = await firstBash;
|
||||
const runningAfterFirstSettles = harness.session.isBashRunning;
|
||||
|
||||
harness.session.abortBash();
|
||||
const secondWasAborted = invocations[1].signal?.aborted;
|
||||
invocations[1].finish();
|
||||
const secondResult = await secondBash;
|
||||
|
||||
expect(firstResult.cancelled).toBe(false);
|
||||
expect(runningAfterFirstSettles).toBe(true);
|
||||
expect(secondWasAborted).toBe(true);
|
||||
expect(secondResult.cancelled).toBe(true);
|
||||
expect(harness.session.isBashRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("aborts all active bash executions", async () => {
|
||||
const harness = await createHarness();
|
||||
harnesses.push(harness);
|
||||
const invocations: ControlledBashInvocation[] = [];
|
||||
const operations = createControlledBashOperations(invocations);
|
||||
|
||||
const firstBash = harness.session.executeBash("first", undefined, { operations });
|
||||
const secondBash = harness.session.executeBash("second", undefined, { operations });
|
||||
|
||||
harness.session.abortBash();
|
||||
const abortedSignals = invocations.map((invocation) => invocation.signal?.aborted);
|
||||
for (const invocation of invocations) {
|
||||
invocation.finish();
|
||||
}
|
||||
const results = await Promise.all([firstBash, secondBash]);
|
||||
|
||||
expect(abortedSignals).toEqual([true, true]);
|
||||
expect(results.map((result) => result.cancelled)).toEqual([true, true]);
|
||||
expect(harness.session.isBashRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("persists user, assistant, toolResult, and custom messages in order", async () => {
|
||||
const echoTool: AgentTool = {
|
||||
name: "echo",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue