Gateway: track background task lifecycle (#52518)

Merged via squash.

Prepared head SHA: 7c4554204e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-03-29 12:48:02 +02:00
committed by GitHub
parent 270d0c5158
commit 17c36b5093
38 changed files with 3548 additions and 58 deletions

View File

@@ -39,6 +39,8 @@ vi.mock("./register.status-health-sessions.js", () => ({
program.command("status");
program.command("health");
program.command("sessions");
const tasks = program.command("tasks");
tasks.command("show");
},
}));
@@ -75,6 +77,7 @@ describe("command-registry", () => {
expect(names).toContain("agents");
expect(names).toContain("backup");
expect(names).toContain("sessions");
expect(names).toContain("tasks");
expect(names).not.toContain("agent");
expect(names).not.toContain("status");
expect(names).not.toContain("doctor");
@@ -139,6 +142,7 @@ describe("command-registry", () => {
expect(names).toContain("status");
expect(names).toContain("health");
expect(names).toContain("sessions");
expect(names).toContain("tasks");
});
it("replaces placeholders when loading a grouped entry by secondary command name", async () => {

View File

@@ -197,6 +197,11 @@ const coreEntries: CoreCliEntry[] = [
description: "List stored conversation sessions",
hasSubcommands: true,
},
{
name: "tasks",
description: "Inspect durable background task state",
hasSubcommands: true,
},
],
register: async ({ program }) => {
const mod = await import("./register.status-health-sessions.js");

View File

@@ -81,6 +81,11 @@ export const CORE_CLI_COMMAND_DESCRIPTORS = [
description: "List stored conversation sessions",
hasSubcommands: true,
},
{
name: "tasks",
description: "Inspect durable background task state",
hasSubcommands: true,
},
] as const satisfies ReadonlyArray<CoreCliCommandDescriptor>;
export function getCoreCliCommandDescriptors(): ReadonlyArray<CoreCliCommandDescriptor> {

View File

@@ -6,6 +6,10 @@ const statusCommand = vi.fn();
const healthCommand = vi.fn();
const sessionsCommand = vi.fn();
const sessionsCleanupCommand = vi.fn();
const tasksListCommand = vi.fn();
const tasksShowCommand = vi.fn();
const tasksNotifyCommand = vi.fn();
const tasksCancelCommand = vi.fn();
const setVerbose = vi.fn();
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
@@ -26,6 +30,13 @@ vi.mock("../../commands/sessions-cleanup.js", () => ({
sessionsCleanupCommand,
}));
vi.mock("../../commands/tasks.js", () => ({
tasksListCommand,
tasksShowCommand,
tasksNotifyCommand,
tasksCancelCommand,
}));
vi.mock("../../globals.js", () => ({
setVerbose,
}));
@@ -55,6 +66,10 @@ describe("registerStatusHealthSessionsCommands", () => {
healthCommand.mockResolvedValue(undefined);
sessionsCommand.mockResolvedValue(undefined);
sessionsCleanupCommand.mockResolvedValue(undefined);
tasksListCommand.mockResolvedValue(undefined);
tasksShowCommand.mockResolvedValue(undefined);
tasksNotifyCommand.mockResolvedValue(undefined);
tasksCancelCommand.mockResolvedValue(undefined);
});
it("runs status command with timeout and debug-derived verbose", async () => {
@@ -201,4 +216,52 @@ describe("registerStatusHealthSessionsCommands", () => {
runtime,
);
});
it("runs tasks list from the parent command", async () => {
await runCli(["tasks", "--json", "--runtime", "acp", "--status", "running"]);
expect(tasksListCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
runtime: "acp",
status: "running",
}),
runtime,
);
});
it("runs tasks show subcommand with lookup forwarding", async () => {
await runCli(["tasks", "show", "run-123", "--json"]);
expect(tasksShowCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
json: true,
}),
runtime,
);
});
it("runs tasks notify subcommand with lookup and policy forwarding", async () => {
await runCli(["tasks", "notify", "run-123", "state_changes"]);
expect(tasksNotifyCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
notify: "state_changes",
}),
runtime,
);
});
it("runs tasks cancel subcommand with lookup forwarding", async () => {
await runCli(["tasks", "cancel", "run-123"]);
expect(tasksCancelCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
}),
runtime,
);
});
});

View File

@@ -3,6 +3,12 @@ import { healthCommand } from "../../commands/health.js";
import { sessionsCleanupCommand } from "../../commands/sessions-cleanup.js";
import { sessionsCommand } from "../../commands/sessions.js";
import { statusCommand } from "../../commands/status.js";
import {
tasksCancelCommand,
tasksListCommand,
tasksNotifyCommand,
tasksShowCommand,
} from "../../commands/tasks.js";
import { setVerbose } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
@@ -213,4 +219,106 @@ export function registerStatusHealthSessionsCommands(program: Command) {
);
});
});
const tasksCmd = program
.command("tasks")
.description("Inspect durable background task state")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by runtime (subagent, acp, cli)")
.option(
"--status <name>",
"Filter by status (accepted, running, done, failed, timed_out, cancelled, lost)",
)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json),
runtime: opts.runtime as string | undefined,
status: opts.status as string | undefined,
},
defaultRuntime,
);
});
});
tasksCmd.enablePositionalOptions();
tasksCmd
.command("list")
.description("List tracked background tasks")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by runtime (subagent, acp, cli)")
.option(
"--status <name>",
"Filter by status (accepted, running, done, failed, timed_out, cancelled, lost)",
)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as
| {
json?: boolean;
runtime?: string;
status?: string;
}
| undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json || parentOpts?.json),
runtime: (opts.runtime as string | undefined) ?? parentOpts?.runtime,
status: (opts.status as string | undefined) ?? parentOpts?.status,
},
defaultRuntime,
);
});
});
tasksCmd
.command("show")
.description("Show one background task by task id, run id, or session key")
.argument("<lookup>", "Task id, run id, or session key")
.option("--json", "Output as JSON", false)
.action(async (lookup, opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksShowCommand(
{
lookup,
json: Boolean(opts.json || parentOpts?.json),
},
defaultRuntime,
);
});
});
tasksCmd
.command("notify")
.description("Set task notify policy")
.argument("<lookup>", "Task id, run id, or session key")
.argument("<notify>", "Notify policy (done_only, state_changes, silent)")
.action(async (lookup, notify) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksNotifyCommand(
{
lookup,
notify: notify as "done_only" | "state_changes" | "silent",
},
defaultRuntime,
);
});
});
tasksCmd
.command("cancel")
.description("Cancel a running background task")
.argument("<lookup>", "Task id, run id, or session key")
.action(async (lookup) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksCancelCommand(
{
lookup,
},
defaultRuntime,
);
});
});
}