Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] 679edaf10e test(server): capture supervisor output when readiness fails
The readiness error previously interpolated output before waiting, losing the startup failure that CI needed to diagnose. Read the captured output and child exit state only when the wait fails, preserving the deadline and lifecycle assertions.
2026-11-11 11:29:21 +00:00
paseo-bot[bot]andKeeva Speyer 00693ec201 fix(server): accept Stop for saved agents without a runtime
A saved closed agent remains addressable after restart even before its provider is loaded. Recognize that record without reopening or rewriting it, preserving metadata and existing startup recovery ownership.

Co-authored-by: Keeva Speyer <keeva@10xlabs.ai>
2026-11-11 10:57:09 +00:00
paseo-bot[bot] 61b243d3cc Merge remote-tracking branch 'origin/main' into diagnose/9369-6f520ace 2026-11-11 10:51:47 +00:00
Keeva Speyer 0a24f731a6 fix(server): cancel reconciles a stored agent that has no live runtime
Stop reports "Agent <id> not found" for an agent whose record says
running but whose runtime is gone (crash, eviction, restart without
resume), while list/status keep showing it as running. Look the record
up in storage: unknown ids still throw, but a stored record with no
live run now reconciles a stale running/initializing status to closed
and reports not_running instead of throwing.
2026-10-03 14:21:07 +10:00
7 changed files with 90 additions and 15 deletions
@@ -170,7 +170,10 @@ describe("agent lifecycle commands", () => {
manager.liveAgents.set("agent-1", managedAgent("agent-1", "running"));
manager.inFlightAgentIds.add("agent-1");
const result = await cancelAgentRunCommand({ agentManager: manager, logger }, "agent-1");
const result = await cancelAgentRunCommand(
{ agentManager: manager, agentStorage: storage, logger },
"agent-1",
);
expect(result).toEqual({
agent: manager.liveAgents.get("agent-1"),
@@ -187,13 +190,46 @@ describe("agent lifecycle commands", () => {
manager.settledDuringCancellationAgentIds.add("agent-1");
await expect(
cancelAgentRunCommand({ agentManager: manager, logger }, "agent-1"),
cancelAgentRunCommand({ agentManager: manager, agentStorage: storage, logger }, "agent-1"),
).resolves.toEqual({
agent: manager.liveAgents.get("agent-1"),
cancelled: false,
});
});
test.each(["closed", "idle", "error", "running", "initializing"] as const)(
"accepts Stop on a stored %s agent without modifying its record",
async (lastStatus) => {
const storage = new FakeLifecycleAgentStorage();
const manager = new FakeLifecycleAgentManager(storage);
const record = { ...storedAgent("agent-1"), lastStatus };
storage.records.set("agent-1", record);
await expect(
cancelAgentRunCommand({ agentManager: manager, agentStorage: storage, logger }, "agent-1"),
).resolves.toEqual({
agent: { id: "agent-1", cwd: record.cwd, lifecycle: lastStatus },
cancelled: false,
});
expect(manager.cancelledAgentIds).toEqual([]);
expect(storage.upserts).toEqual([]);
expect(storage.records.get("agent-1")).toEqual(record);
},
);
test("still reports not found when no stored record exists", async () => {
const storage = new FakeLifecycleAgentStorage();
const manager = new FakeLifecycleAgentManager(storage);
await expect(
cancelAgentRunCommand(
{ agentManager: manager, agentStorage: storage, logger },
"missing-agent",
),
).rejects.toThrow("Agent missing-agent not found");
expect(storage.upserts).toEqual([]);
});
test("archives a live agent after canceling and clearing attention", async () => {
const storage = new FakeLifecycleAgentStorage();
const manager = new FakeLifecycleAgentManager(storage);
@@ -56,14 +56,13 @@ interface RequestedAgentRunCancellation extends CancelAgentRunResult {
}
async function requestAgentRunCancellation(
dependencies: Pick<AgentLifecycleCommandDependencies, "agentManager" | "logger">,
dependencies: Pick<AgentLifecycleCommandDependencies, "agentManager" | "agentStorage" | "logger">,
agentId: string,
): Promise<RequestedAgentRunCancellation> {
const { agentManager, logger } = dependencies;
const agent = agentManager.getAgent(agentId);
if (!agent) {
logger.trace({ agentId }, "cancelAgentRunCommand: agent not found");
throw new Error(`Agent ${agentId} not found`);
return storedAgentWithoutRun(dependencies, agentId);
}
const hasInFlightRun = agentManager.hasInFlightRun(agentId);
@@ -93,8 +92,30 @@ async function requestAgentRunCancellation(
};
}
async function storedAgentWithoutRun(
dependencies: Pick<AgentLifecycleCommandDependencies, "agentStorage" | "logger">,
agentId: string,
): Promise<RequestedAgentRunCancellation> {
const { agentStorage, logger } = dependencies;
const record = await agentStorage.get(agentId);
if (!record) {
logger.trace({ agentId }, "cancelAgentRunCommand: agent not found");
throw new Error(`Agent ${agentId} not found`);
}
logger.trace(
{ agentId, lastStatus: record.lastStatus },
"cancelAgentRunCommand: stored agent has no live runtime",
);
return {
agent: { id: record.id, cwd: record.cwd, lifecycle: record.lastStatus },
cancelled: false,
cancellation: { status: "not_running" },
};
}
export async function cancelAgentRunCommand(
dependencies: Pick<AgentLifecycleCommandDependencies, "agentManager" | "logger">,
dependencies: Pick<AgentLifecycleCommandDependencies, "agentManager" | "agentStorage" | "logger">,
agentId: string,
): Promise<CancelAgentRunResult> {
const result = await requestAgentRunCancellation(dependencies, agentId);
@@ -2226,7 +2226,7 @@ function createToolCatalog(
},
async ({ agentId }) => {
const { cancelled } = await cancelAgentRunCommand(
{ agentManager, logger: childLogger },
{ agentManager, agentStorage, logger: childLogger },
agentId,
);
return {
@@ -1155,6 +1155,14 @@ test("disabled recovery closes stale active records without resuming sessions",
expect(
Object.fromEntries(snapshot.entries.map((entry) => [entry.agent.id, entry.agent.status])),
).toEqual(Object.fromEntries(expected.map((record) => [record.id, record.lastStatus])));
const closedId = records.find((record) => record.lastStatus === "closed")!.id;
const beforeStop = await handle.daemon.agentStorage.get(closedId);
await expect(client.cancelAgent(closedId)).resolves.toBeUndefined();
await expect(client.cancelAgent(closedId)).resolves.toBeUndefined();
expect(await handle.daemon.agentStorage.get(closedId)).toEqual(beforeStop);
expect(handle.daemon.agentManager.listAgents()).toEqual([]);
expect(sessionsCreated).toBe(0);
await expect(client.cancelAgent("missing-agent")).rejects.toThrow("not found");
const opened = await ensureAgentLoaded(records[0]!.id, {
agentManager: handle.daemon.agentManager,
agentStorage: handle.daemon.agentStorage,
+2 -1
View File
@@ -1387,7 +1387,8 @@ export async function createPaseoDaemon(
agentManager,
agentStorage,
createAgent,
interruptAgent: (agentId) => cancelAgentRunCommand({ agentManager, logger }, agentId),
interruptAgent: (agentId) =>
cancelAgentRunCommand({ agentManager, agentStorage, logger }, agentId),
archiveWorkspace: archiveWorkspaceByIdExternal,
cleanupFailedCreate: (input) =>
hubAgentLifecycle.cleanupCreatedWorktreeAfterFailedAgentCreate(input),
@@ -64,7 +64,7 @@ function isAlive(pid: number): boolean {
async function waitFor<T>(
probe: () => Promise<T | null | undefined | false> | T | null | undefined | false,
message: string,
message: string | (() => string),
timeoutMs = READY_TIMEOUT_MS,
): Promise<T> {
const deadline = Date.now() + timeoutMs;
@@ -78,7 +78,8 @@ async function waitFor<T>(
}
await delay(100);
}
throw new Error(`${message}${lastError ? ` (last error: ${String(lastError)})` : ""}`);
const detail = typeof message === "function" ? message() : message;
throw new Error(`${detail}${lastError ? ` (last error: ${String(lastError)})` : ""}`);
}
async function getAvailablePort(): Promise<number> {
@@ -123,10 +124,14 @@ async function startSupervisor(
supervisor.stderr?.on("data", (chunk) => (output = (output + String(chunk)).slice(-20_000)));
const harness: Harness = { home, workDir, port, supervisor, output: () => output };
harnesses.push(harness);
await waitFor(async () => {
const instance = await readDaemonInstance(home);
return instance?.pid === supervisor.pid && instance.listen ? instance : null;
}, `supervisor did not become ready:\n${output}`);
await waitFor(
async () => {
const instance = await readDaemonInstance(home);
return instance?.pid === supervisor.pid && instance.listen ? instance : null;
},
() =>
`supervisor did not become ready (exit=${supervisor.exitCode}, signal=${supervisor.signalCode}):\n${output}`,
);
return harness;
}
+5 -1
View File
@@ -5230,7 +5230,11 @@ export class Session {
try {
await cancelAgentRunCommand(
{ agentManager: this.agentManager, logger: this.sessionLogger },
{
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
},
agentId,
);
if (requestId) {