Skip to content

Commit a643ef4

Browse files
author
build_agent
committed
fix_smartCommand
1 parent 7c0a222 commit a643ef4

3 files changed

Lines changed: 44 additions & 1 deletion

File tree

app/backend/src/agent/commandLog.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventEmitter } from "node:events";
2+
import { killBackgroundProcess } from "../tools/smartCommand.js";
23

34
/**
45
* Tracks every shell command the agent runs through the `run_command` tool so
@@ -17,6 +18,8 @@ export interface AgentCommandRun {
1718
stdout: string;
1819
stderr: string;
1920
truncated: boolean;
21+
/** PID of the child process, set for background (long-running) commands. */
22+
pid?: number;
2023
}
2124

2225
export type AgentCommandSummary = Omit<AgentCommandRun, "stdout" | "stderr"> & {
@@ -27,14 +30,16 @@ export type AgentCommandSummary = Omit<AgentCommandRun, "stdout" | "stderr"> & {
2730
/** Lightweight token for an in-progress agent command. */
2831
export interface PendingCommandHandle {
2932
readonly id: string;
33+
/** Register the OS PID once the child spawns so dismiss can kill it. */
34+
setPid(pid: number): void;
3035
appendChunk(stream: "stdout" | "stderr", text: string): void;
3136
complete(result: Omit<AgentCommandRun, "id">): AgentCommandRun;
3237
}
3338

3439
const MAX_RUNS = 100;
3540
const ring: AgentCommandRun[] = [];
3641
/** In-progress runs — kept until complete() is called so reconnecting clients can replay them. */
37-
const pending = new Map<string, { id: string; cmd: string; cwd: string; startedAt: number; output: string }>();
42+
const pending = new Map<string, { id: string; cmd: string; cwd: string; startedAt: number; output: string; pid?: number }>();
3843
const bus = new EventEmitter();
3944
bus.setMaxListeners(50);
4045

@@ -65,6 +70,10 @@ export function startAgentCommand(cmd: string, cwd: string): PendingCommandHandl
6570

6671
const handle: PendingCommandHandle = {
6772
id,
73+
setPid(pid: number) {
74+
const p = pending.get(id);
75+
if (p) p.pid = pid;
76+
},
6877
appendChunk(stream, text) {
6978
const p = pending.get(id);
7079
if (p) p.output += text;
@@ -112,15 +121,33 @@ export function getAgentCommand(id: string): AgentCommandRun | undefined {
112121
}
113122

114123
export function clearAgentCommands(): number {
124+
// Kill any background PIDs still tracked on the ring before clearing.
125+
for (const r of ring) {
126+
if (r.pid != null) killBackgroundProcess(r.pid);
127+
}
128+
// Also kill in-progress commands (pending map may have pids for queued long-runners).
129+
for (const p of pending.values()) {
130+
if (p.pid != null) killBackgroundProcess(p.pid);
131+
}
115132
const n = ring.length;
116133
ring.length = 0;
117134
bus.emit("clear");
118135
return n;
119136
}
120137

121138
export function deleteAgentCommand(id: string): boolean {
139+
// Also try pending (user dismissed while the command was still running).
140+
const p = pending.get(id);
141+
if (p) {
142+
if (p.pid != null) killBackgroundProcess(p.pid);
143+
pending.delete(id);
144+
bus.emit("delete", id);
145+
return true;
146+
}
122147
const i = ring.findIndex((r) => r.id === id);
123148
if (i < 0) return false;
149+
const entry = ring[i];
150+
if (entry.pid != null) killBackgroundProcess(entry.pid);
124151
ring.splice(i, 1);
125152
bus.emit("delete", id);
126153
return true;

app/backend/src/agent/executor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export async function executeTool(
275275
try {
276276
r = await runSmartCommand(cmd, {
277277
cwd: getWorkspace(),
278+
onChildSpawn: (pid) => cmdHandle.setPid(pid),
278279
onStreamChunk: (stream, text) => {
279280
if (stream === "out") qOut += text;
280281
else qErr += text;
@@ -298,6 +299,7 @@ export async function executeTool(
298299
stdout: r.stdout,
299300
stderr: r.stderr,
300301
truncated: r.truncated,
302+
pid: r.pid,
301303
});
302304

303305
// Build output summary based on result mode

app/backend/src/tools/smartCommand.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ export interface SmartCommandOptions {
152152
* Used to forward `command_chunk` to the UI without blocking the agent on a poll loop.
153153
*/
154154
onStreamChunk?: (stream: "out" | "err", text: string) => void | Promise<void>;
155+
/**
156+
* Called immediately after the child process spawns with its PID.
157+
* Lets callers register the pid for cleanup before the command resolves
158+
* (important for long-running "background" commands where the promise may
159+
* resolve early and the caller needs to be able to kill the process later).
160+
*/
161+
onChildSpawn?: (pid: number) => void;
155162
}
156163

157164
// Keep track of background processes so we can clean them up
@@ -226,6 +233,7 @@ export async function runSmartCommand(
226233
if (!trimmed) throw new Error("Empty command");
227234

228235
const streamCb = opts.onStreamChunk;
236+
const spawnCb = opts.onChildSpawn;
229237
const cwd = opts.cwd ?? getWorkspace();
230238
const maxBytes = opts.maxBytes ?? 256 * 1024;
231239
const isLongRunning = opts.forceLongRunning || isLongRunningCommand(trimmed);
@@ -263,6 +271,12 @@ export async function runSmartCommand(
263271
...(killAsGroup ? { detached: true } : {}),
264272
});
265273

274+
// Notify caller of the PID as soon as the child is alive so they can
275+
// register it for potential early-kill (e.g. user dismisses the run).
276+
if (child.pid !== undefined && spawnCb) {
277+
try { spawnCb(child.pid); } catch { /* caller must not throw */ }
278+
}
279+
266280
let outBuf = "";
267281
let errBuf = "";
268282
let truncated = false;

0 commit comments

Comments
 (0)