Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,9 @@ ade chat create --lane lane-id --provider codex --no-parent # spawned chats de
ade chat read session-id --limit 20 --text
ade chat message session-id --kind auto --text "status/context"
ade chat steer session-id --text "active-turn context"
ade chat schedules session-id --pause # pause this chat's durable wakeups/cron/loops (omit flag to inspect, --resume to re-arm)
ade chat schedules session-id --pause # pause this agent session's durable wakeups/cron/loops (omit flag to inspect, --resume to re-arm)
ade chat scheduled-work list [session-id] --all # list durable jobs; --all includes recent terminal history
ade chat scheduled-work create --cron "9,29,49 * * * *" --prompt "Check CI and report" --once --reason "CI check" --session session-id # chats and tracked provider CLIs; omit --session inside the bound agent
ade chat scheduled-work cancel session-id job-id # cancel one job; Claude-native jobs request CronDelete in the owning chat
ade chat wait session-id --for idle --timeout-ms 600000
ade chat recover session-id --turn turn-id --action nudge # wait | nudge | retry | resume
Expand Down
116 changes: 116 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,21 @@ function createRuntime() {
lastActivityAt: "2026-03-17T19:00:00.000Z",
createdAt: "2026-03-17T19:00:00.000Z",
})),
createScheduledWork: vi.fn(async ({ sessionId, cron, prompt, recurring = true }: {
sessionId: string;
cron: string;
prompt: string;
recurring?: boolean;
}) => ({
item: {
id: `action:${sessionId}:schedule-1`,
sessionId,
cron,
prompt,
kind: recurring ? "cron" : "wakeup",
status: "scheduled",
},
})),
listScheduledWork: vi.fn(async ({ sessionId, includeTerminal }: {
sessionId?: string;
includeTerminal?: boolean;
Expand All @@ -498,6 +513,12 @@ function createRuntime() {
kind: "cron",
status: includeTerminal ? "cancelled" : "scheduled",
}]),
getScheduledWorkState: vi.fn(async ({ sessionId }: { sessionId: string }) => ({
sessionId,
paused: false,
nextWakeAt: "2026-03-17T20:00:00.000Z",
items: [],
})),
cancelScheduledWork: vi.fn(async ({ sessionId, scheduleId }: {
sessionId: string;
scheduleId: string;
Expand All @@ -506,6 +527,10 @@ function createRuntime() {
providerCancellationRequested: false,
providerCancellationConfirmed: true,
})),
setScheduledWorkPaused: vi.fn(async ({ sessionId, paused }: {
sessionId: string;
paused: boolean;
}) => ({ sessionId, paused, nextWakeAt: null })),
getChatTranscript: vi.fn(async ({ sessionId }: { sessionId: string }) => ({
sessionId,
entries: [{ role: "assistant", text: "hello", timestamp: "2026-03-17T19:00:00.000Z" }],
Expand Down Expand Up @@ -2811,6 +2836,16 @@ describe("adeRpcServer", () => {
expect(allScheduledWork?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.listScheduledWork).toHaveBeenCalledWith({});

const scheduledWorkState = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "getScheduledWorkState",
args: { sessionId: " chat-1 " },
});
expect(scheduledWorkState?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.getScheduledWorkState).toHaveBeenCalledWith({
sessionId: "chat-1",
});

const cancelledWork = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "cancelScheduledWork",
Expand Down Expand Up @@ -3194,6 +3229,63 @@ describe("adeRpcServer", () => {
text: "own-chat write",
});

const ownScheduledWorkCreate = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "createScheduledWork",
args: { cron: "0 * * * *", prompt: "Check CI." },
});
expect(ownScheduledWorkCreate?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.createScheduledWork).toHaveBeenCalledWith({
sessionId: "chat-1",
cron: "0 * * * *",
prompt: "Check CI.",
});

const deniedScheduledWorkCreate = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "createScheduledWork",
args: { sessionId: "chat-2", cron: "0 * * * *", prompt: "Cross-chat schedule." },
});
expect(deniedScheduledWorkCreate.isError).toBe(true);
expect(fixture.runtime.agentChatService.createScheduledWork).toHaveBeenCalledTimes(1);

const ownScheduledWorkState = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "getScheduledWorkState",
args: {},
});
expect(ownScheduledWorkState?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.getScheduledWorkState).toHaveBeenCalledWith({
sessionId: "chat-1",
});

const deniedScheduledWorkState = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "getScheduledWorkState",
args: { sessionId: "chat-2" },
});
expect(deniedScheduledWorkState.isError).toBe(true);
expect(fixture.runtime.agentChatService.getScheduledWorkState).toHaveBeenCalledTimes(1);

const ownScheduledWorkPause = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "setScheduledWorkPaused",
args: { paused: true },
});
expect(ownScheduledWorkPause?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.setScheduledWorkPaused).toHaveBeenCalledWith({
sessionId: "chat-1",
paused: true,
});

const deniedScheduledWorkPause = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "setScheduledWorkPaused",
args: { sessionId: "chat-2", paused: true },
});
expect(deniedScheduledWorkPause.isError).toBe(true);
expect(fixture.runtime.agentChatService.setScheduledWorkPaused).toHaveBeenCalledTimes(1);

const deniedScheduledWorkList = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "listScheduledWork",
Expand Down Expand Up @@ -3825,6 +3917,30 @@ describe("adeRpcServer", () => {
expect(scheduledWorkList.isError).toBe(true);
expect(fixture.runtime.agentChatService.listScheduledWork).not.toHaveBeenCalled();

const scheduledWorkCreate = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "createScheduledWork",
args: { sessionId: "chat-2", cron: "0 * * * *", prompt: "Check CI." },
});
expect(scheduledWorkCreate.isError).toBe(true);
expect(fixture.runtime.agentChatService.createScheduledWork).not.toHaveBeenCalled();

const scheduledWorkState = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "getScheduledWorkState",
args: { sessionId: "chat-2" },
});
expect(scheduledWorkState.isError).toBe(true);
expect(fixture.runtime.agentChatService.getScheduledWorkState).not.toHaveBeenCalled();

const scheduledWorkPause = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "setScheduledWorkPaused",
args: { sessionId: "chat-2", paused: true },
});
expect(scheduledWorkPause.isError).toBe(true);
expect(fixture.runtime.agentChatService.setScheduledWorkPaused).not.toHaveBeenCalled();

const scheduledWorkCancel = await callTool(handler, "run_ade_action", {
domain: "chat",
action: "cancelScheduledWork",
Expand Down
24 changes: 12 additions & 12 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2382,18 +2382,23 @@ function scopeTerminalAdeActionArgs(
}
}

const SCOPED_CHAT_ACTIONS = new Set([
"readTranscript",
"sendMessage",
"createScheduledWork",
"listScheduledWork",
"getScheduledWorkState",
"cancelScheduledWork",
"setScheduledWorkPaused",
]);

function scopeChatAdeActionArgs(
session: SessionState,
action: string,
chatArgs: Record<string, unknown>,
): Record<string, unknown> {
const method = `run_ade_action:chat.${action}`;
if (
action !== "readTranscript"
&& action !== "sendMessage"
&& action !== "listScheduledWork"
&& action !== "cancelScheduledWork"
) return chatArgs;
if (!SCOPED_CHAT_ACTIONS.has(action)) return chatArgs;
if (isUnboundAdeCliCaller(session)) return chatArgs;

const scopedArgs = { ...chatArgs };
Expand Down Expand Up @@ -3481,12 +3486,7 @@ async function runTool(args: {
} else if (
!callerIsCto
&& domain === "chat"
&& (
action === "readTranscript"
|| action === "sendMessage"
|| action === "listScheduledWork"
|| action === "cancelScheduledWork"
)
&& SCOPED_CHAT_ACTIONS.has(action)
) {
scopedObjectArgs = scopeChatAdeActionArgs(
session,
Expand Down
67 changes: 65 additions & 2 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4169,8 +4169,8 @@ describe("ADE CLI", () => {
expect(inspect.steps[0]?.params).toMatchObject({
arguments: {
domain: "chat",
action: "getSessionSummary",
argsList: ["chat-1"],
action: "getScheduledWorkState",
args: { sessionId: "chat-1" },
},
});

Expand Down Expand Up @@ -4198,6 +4198,69 @@ describe("ADE CLI", () => {
},
});

const create = expectExecutePlan(buildCliPlan([
"chat",
"scheduled-work",
"create",
"--cron",
"9,29,49 * * * *",
"--prompt",
"Check CI and report",
]));
expect(create.label).toBe("chat scheduled-work create");
expect(create.steps[0]?.params).toMatchObject({
arguments: {
domain: "chat",
action: "createScheduledWork",
args: {
cron: "9,29,49 * * * *",
prompt: "Check CI and report",
recurring: true,
},
},
});

const createOnce = expectExecutePlan(buildCliPlan([
"chat",
"scheduled-work",
"create",
"--cron",
"15 10 * * 1",
"--prompt",
"Prepare the weekly report",
"--once",
"--reason",
"Weekly report",
"--session",
"chat-1",
]));
expect(createOnce.steps[0]?.params).toMatchObject({
arguments: {
action: "createScheduledWork",
args: {
sessionId: "chat-1",
cron: "15 10 * * 1",
prompt: "Prepare the weekly report",
recurring: false,
reason: "Weekly report",
},
},
});
expect(() => buildCliPlan([
"chat",
"scheduled-work",
"create",
"--prompt",
"Missing cron",
])).toThrow(/cron is required/);
expect(() => buildCliPlan([
"chat",
"scheduled-work",
"create",
"--cron",
"0 * * * *",
])).toThrow(/prompt is required/);

for (const alias of ["schedule", "schedules"]) {
expect(expectExecutePlan(buildCliPlan(["chat", alias, "list", "chat-1"])).steps[0]?.params)
.toMatchObject({
Expand Down
42 changes: 33 additions & 9 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1641,9 +1641,11 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade chat rewind-files <session> --message <user-message-id> --dry-run
Preview or apply file/context rewind
$ ade chat subagents <session> --text List child agents for a chat
$ ade chat schedules <session> --pause Pause this chat's durable wakeups/cron/loops
$ ade chat schedules <session> --pause Pause this agent session's durable wakeups/cron/loops
$ ade chat schedules <session> Inspect pause state + next armed wake (--resume to re-arm)
$ ade chat scheduled-work list [session] List durable jobs (--all includes recent history)
$ ade chat scheduled-work create --cron "<expr>" --prompt "<text>" [--once]
Optional: --reason "<text>" --session <id>
$ ade chat scheduled-work cancel <session> <id> Cancel one job; Claude crons also request CronDelete
$ ade new chat --mode cli --lane <lane> --provider claude --reasoning-effort ultracode --prompt "fix"
Start a tracked provider CLI session
Expand Down Expand Up @@ -6633,7 +6635,7 @@ function buildChatPlan(args: string[]): CliPlan {
|| sub === "schedules"
|| sub === "schedule"
)
&& (args[0] === "list" || args[0] === "cancel")
&& (args[0] === "list" || args[0] === "create" || args[0] === "cancel")
? firstStandalonePositional(args)
: null;
const sessionId =
Expand Down Expand Up @@ -7280,6 +7282,29 @@ function buildChatPlan(args: string[]): CliPlan {
sub === "schedule" ||
sub === "scheduled-work"
) {
if (scheduledWorkOperation === "create") {
const cron = requireValue(readValue(args, ["--cron"]), "cron");
const prompt = requireValue(readValue(args, ["--prompt", "--text"]), "prompt");
const reason = readValue(args, ["--reason"]);
return {
kind: "execute",
label: "chat scheduled-work create",
steps: [
actionStep(
"result",
"chat",
"createScheduledWork",
collectGenericObjectArgs(args, {
...(sessionId ? { sessionId } : {}),
cron,
prompt,
recurring: !readFlag(args, ["--once"]),
...(reason ? { reason } : {}),
}),
),
],
};
}
if (scheduledWorkOperation === "list") {
return {
kind: "execute",
Expand Down Expand Up @@ -7316,24 +7341,22 @@ function buildChatPlan(args: string[]): CliPlan {
],
};
}
// Per-chat scheduled-work control: pause/resume this chat's durable
// wakeups/cron/loop schedules, or inspect (no flag) the current pause
// state + next armed wake via getSessionSummary.
// Per-session scheduled-work control. The state query works for both chat
// sessions and tracked provider CLI terminals.
const targetSession = requireValue(sessionId, "sessionId");
const pause = readFlag(args, ["--pause", "--pause-scheduled-work"]);
const resume = readFlag(args, ["--resume", "--unpause"]);
if (pause && resume) {
throw new CliUsageError("Use either --pause or --resume, not both.");
}
if (!pause && !resume) {
// Inspection mode: summary carries nextWakeAt + scheduledWorkPaused.
return {
kind: "execute",
label: "chat schedules",
steps: [
actionArgsListStep("result", "chat", "getSessionSummary", [
targetSession,
]),
actionStep("result", "chat", "getScheduledWorkState", {
sessionId: targetSession,
}),
],
};
}
Expand Down Expand Up @@ -11242,6 +11265,7 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
"--compare-to",
"--content",
"--context-file",
"--cron",
"--cwd",
"--data",
"--cpu",
Expand Down
Loading
Loading