mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(agent-core-v2): cap detached process output
This commit is contained in:
parent
2e2ca0362f
commit
3e8fca371b
2 changed files with 26 additions and 24 deletions
|
|
@ -96,9 +96,8 @@ interface ManagedTask {
|
|||
outputSizeBytes: number;
|
||||
retainedOutputBytes: number;
|
||||
/**
|
||||
* True once a foreground command has crossed `MAX_TASK_OUTPUT_BYTES` and
|
||||
* termination has been requested. One-shot guard so the ceiling fires exactly
|
||||
* once.
|
||||
* True once a command has crossed `MAX_TASK_OUTPUT_BYTES` and termination has
|
||||
* been requested. One-shot guard so the ceiling fires exactly once.
|
||||
*/
|
||||
outputLimitTripped: boolean;
|
||||
status: AgentTaskStatus;
|
||||
|
|
@ -126,16 +125,18 @@ interface ManagedTask {
|
|||
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MiB
|
||||
|
||||
/**
|
||||
* Hard ceiling on the combined output a single foreground shell command may
|
||||
* stream before it is force-terminated (SIGTERM → grace → SIGKILL). It guards
|
||||
* the live-forward path from a runaway command (e.g. `b3sum --length <huge>`)
|
||||
* whose output would otherwise grow without bound until Node aborts with an
|
||||
* out-of-memory crash. Detached process tasks are exempt because their output
|
||||
* is consumed through the bounded live ring plus `output.log`.
|
||||
* Hard ceiling on the combined output a single shell command may stream before
|
||||
* it is force-terminated (SIGTERM → grace → SIGKILL). It guards both the
|
||||
* live-forward path and the on-disk `output.log` write chain from a runaway
|
||||
* command (e.g. `b3sum --length <huge>`) whose output would otherwise grow
|
||||
* without bound, filling the disk or retaining each pending-write chunk until
|
||||
* Node aborts with an out-of-memory crash. Scoped to process tasks (foreground
|
||||
* and background); subagent and user-question results are appended once and must
|
||||
* always be persisted, so they are intentionally not capped here.
|
||||
*/
|
||||
const MAX_TASK_OUTPUT_BYTES = 16 * 1024 * 1024; // 16 MiB
|
||||
|
||||
/** Terminal `stopReason` recorded when a foreground command trips the output ceiling. */
|
||||
/** Terminal `stopReason` recorded when a command trips the output ceiling. */
|
||||
function outputLimitReason(): string {
|
||||
const mib = Math.floor(MAX_TASK_OUTPUT_BYTES / (1024 * 1024));
|
||||
return (
|
||||
|
|
@ -827,14 +828,15 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
entry.outputSizeBytes += chunkBytes;
|
||||
this.appendRetainedOutput(entry, chunk, chunkBytes);
|
||||
|
||||
// Foreground output ceiling: a single non-detached shell command must not
|
||||
// grow the unbounded live-forward buffer until the process runs out of
|
||||
// memory. Trip once, then request graceful termination through the shared
|
||||
// stop path (SIGTERM → grace → SIGKILL). Detached tasks are exempt because
|
||||
// their output is consumed through the bounded live ring plus output.log.
|
||||
// Output ceiling: a single shell command must not grow the unbounded
|
||||
// live-forward buffer or the on-disk write chain until the process runs out
|
||||
// of memory or fills the disk. Trip once, then request graceful termination
|
||||
// through the shared stop path (SIGTERM → grace → SIGKILL). Scoped to
|
||||
// process tasks (foreground and background): subagent and user-question tasks
|
||||
// append their bounded result in one shot and must always persist it, so they
|
||||
// are intentionally not capped here.
|
||||
if (
|
||||
!entry.outputLimitTripped &&
|
||||
!this.isDetached(entry) &&
|
||||
entry.task?.kind === 'process' &&
|
||||
entry.outputSizeBytes > MAX_TASK_OUTPUT_BYTES
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -654,7 +654,7 @@ describe('AgentTaskService', () => {
|
|||
expect(forwardedChars).toBeLessThanOrEqual(LIMIT_BYTES);
|
||||
});
|
||||
|
||||
it('does not terminate a detached process task that exceeds the foreground output limit', async () => {
|
||||
it('also terminates a detached process task that exceeds the output limit', async () => {
|
||||
const { manager } = createAgentTaskService();
|
||||
const chunks = Array.from({ length: 20 }, () => 'x'.repeat(MiB));
|
||||
const { proc, killSpy } = streamingProcess(chunks);
|
||||
|
|
@ -666,9 +666,9 @@ describe('AgentTaskService', () => {
|
|||
|
||||
const info = await waitForTerminal(manager, taskId);
|
||||
|
||||
expect(info).toMatchObject({ status: 'completed' });
|
||||
expect(info?.stopReason).toBeUndefined();
|
||||
expect(killSpy).not.toHaveBeenCalledWith('SIGTERM');
|
||||
expect(info).toMatchObject({ status: 'killed' });
|
||||
expect(info?.stopReason ?? '').toMatch(/output limit/i);
|
||||
expect(killSpy).toHaveBeenCalledWith('SIGTERM');
|
||||
});
|
||||
|
||||
it('stops appending persisted foreground output once the output limit trips', async () => {
|
||||
|
|
@ -697,7 +697,7 @@ describe('AgentTaskService', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('persists detached process output beyond the foreground output limit', async () => {
|
||||
it('stops appending persisted output once the output limit trips for a detached process task', async () => {
|
||||
const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-limit-bg-'));
|
||||
try {
|
||||
const { manager } = createAgentTaskService({ sessionDir });
|
||||
|
|
@ -715,9 +715,9 @@ describe('AgentTaskService', () => {
|
|||
const info = await waitForTerminal(manager, taskId);
|
||||
const output = await manager.getOutputSnapshot(taskId, 1);
|
||||
|
||||
expect(info).toMatchObject({ status: 'completed' });
|
||||
expect(info?.stopReason).toBeUndefined();
|
||||
expect(output.outputSizeBytes).toBe(20 * MiB);
|
||||
expect(info).toMatchObject({ status: 'killed' });
|
||||
expect(info?.stopReason ?? '').toMatch(/output limit/i);
|
||||
expect(output.outputSizeBytes).toBeLessThanOrEqual(LIMIT_BYTES);
|
||||
} finally {
|
||||
await rm(sessionDir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue