Merge b6467585bf into da71eaebd2
This commit is contained in:
commit
8b77301fce
@ -195,7 +195,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
browser: "Control web browser",
|
browser: "Control web browser",
|
||||||
canvas: "Present/eval/snapshot the Canvas",
|
canvas: "Present/eval/snapshot the Canvas",
|
||||||
nodes: "List/describe/notify/camera/screen on paired nodes",
|
nodes: "List/describe/notify/camera/screen on paired nodes",
|
||||||
cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate; for simple text reminders that don't need AI processing, use sessionTarget=\"none\" with payload.kind=\"directMessage\" — zero token cost)",
|
||||||
message: "Send messages and channel actions",
|
message: "Send messages and channel actions",
|
||||||
gateway: "Restart, apply config, or run updates on the running OpenClaw process",
|
gateway: "Restart, apply config, or run updates on the running OpenClaw process",
|
||||||
agents_list: "List agent ids allowed for sessions_spawn",
|
agents_list: "List agent ids allowed for sessions_spawn",
|
||||||
@ -346,7 +346,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
"- browser: control openclaw's dedicated browser",
|
"- browser: control openclaw's dedicated browser",
|
||||||
"- canvas: present/eval/snapshot the Canvas",
|
"- canvas: present/eval/snapshot the Canvas",
|
||||||
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
||||||
"- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
"- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate; for simple text reminders that don't need AI processing, use sessionTarget=\"none\" with payload.kind=\"directMessage\" — zero token cost)",
|
||||||
"- sessions_list: list sessions",
|
"- sessions_list: list sessions",
|
||||||
"- sessions_history: fetch session history",
|
"- sessions_history: fetch session history",
|
||||||
"- sessions_send: send to another session",
|
"- sessions_send: send to another session",
|
||||||
|
|||||||
@ -150,7 +150,7 @@ JOB SCHEMA (for add action):
|
|||||||
"name": "string (optional)",
|
"name": "string (optional)",
|
||||||
"schedule": { ... }, // Required: when to run
|
"schedule": { ... }, // Required: when to run
|
||||||
"payload": { ... }, // Required: what to execute
|
"payload": { ... }, // Required: what to execute
|
||||||
"sessionTarget": "main" | "isolated", // Required
|
"sessionTarget": "main" | "isolated" | "none", // Required
|
||||||
"enabled": true | false // Optional, default true
|
"enabled": true | false // Optional, default true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,10 +167,13 @@ PAYLOAD TYPES (payload.kind):
|
|||||||
{ "kind": "systemEvent", "text": "<message>" }
|
{ "kind": "systemEvent", "text": "<message>" }
|
||||||
- "agentTurn": Runs agent with message (isolated sessions only)
|
- "agentTurn": Runs agent with message (isolated sessions only)
|
||||||
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional>, "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
|
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional>, "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
|
||||||
|
- "directMessage": Sends text directly to a channel WITHOUT AI — zero token cost. Best for simple text reminders.
|
||||||
|
{ "kind": "directMessage", "text": "<message>", "channel": "<optional-channel-id>", "to": "<optional-target>" }
|
||||||
|
|
||||||
CRITICAL CONSTRAINTS:
|
CRITICAL CONSTRAINTS:
|
||||||
- sessionTarget="main" REQUIRES payload.kind="systemEvent"
|
- sessionTarget="main" REQUIRES payload.kind="systemEvent"
|
||||||
- sessionTarget="isolated" REQUIRES payload.kind="agentTurn"
|
- sessionTarget="isolated" REQUIRES payload.kind="agentTurn"
|
||||||
|
- sessionTarget="none" REQUIRES payload.kind="directMessage"
|
||||||
|
|
||||||
WAKE MODES (for wake action):
|
WAKE MODES (for wake action):
|
||||||
- "next-heartbeat" (default): Wake on next heartbeat
|
- "next-heartbeat" (default): Wake on next heartbeat
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export function normalizeCronJobInput(
|
|||||||
const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
|
const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
|
||||||
if (kind === "systemEvent") next.sessionTarget = "main";
|
if (kind === "systemEvent") next.sessionTarget = "main";
|
||||||
if (kind === "agentTurn") next.sessionTarget = "isolated";
|
if (kind === "agentTurn") next.sessionTarget = "isolated";
|
||||||
|
if (kind === "directMessage") next.sessionTarget = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -131,6 +131,16 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP
|
|||||||
return { kind: "systemEvent", text };
|
return { kind: "systemEvent", text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.kind === "directMessage") {
|
||||||
|
if (existing.kind !== "directMessage") {
|
||||||
|
return buildPayloadFromPatch(patch);
|
||||||
|
}
|
||||||
|
const text = typeof patch.text === "string" ? patch.text : existing.text;
|
||||||
|
const channel = typeof patch.channel === "string" ? patch.channel : existing.channel;
|
||||||
|
const to = typeof patch.to === "string" ? patch.to : existing.to;
|
||||||
|
return { kind: "directMessage", text, channel, to };
|
||||||
|
}
|
||||||
|
|
||||||
if (existing.kind !== "agentTurn") {
|
if (existing.kind !== "agentTurn") {
|
||||||
return buildPayloadFromPatch(patch);
|
return buildPayloadFromPatch(patch);
|
||||||
}
|
}
|
||||||
@ -157,6 +167,13 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
|
|||||||
return { kind: "systemEvent", text: patch.text };
|
return { kind: "systemEvent", text: patch.text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.kind === "directMessage") {
|
||||||
|
if (typeof patch.text !== "string" || patch.text.length === 0) {
|
||||||
|
throw new Error('cron.update payload.kind="directMessage" requires text');
|
||||||
|
}
|
||||||
|
return { kind: "directMessage", text: patch.text, channel: patch.channel, to: patch.to };
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof patch.message !== "string" || patch.message.length === 0) {
|
if (typeof patch.message !== "string" || patch.message.length === 0) {
|
||||||
throw new Error('cron.update payload.kind="agentTurn" requires message');
|
throw new Error('cron.update payload.kind="agentTurn" requires message');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,5 +55,6 @@ export function inferLegacyName(job: {
|
|||||||
|
|
||||||
export function normalizePayloadToSystemText(payload: CronPayload) {
|
export function normalizePayloadToSystemText(payload: CronPayload) {
|
||||||
if (payload.kind === "systemEvent") return payload.text.trim();
|
if (payload.kind === "systemEvent") return payload.text.trim();
|
||||||
|
if (payload.kind === "directMessage") return payload.text.trim();
|
||||||
return payload.message.trim();
|
return payload.message.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export type CronServiceDeps = {
|
|||||||
outputText?: string;
|
outputText?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
sendDirectMessage?: (params: { text: string; channel?: string; to?: string }) => Promise<{ ok: boolean; error?: string }>;
|
||||||
onEvent?: (evt: CronEvent) => void;
|
onEvent?: (evt: CronEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,37 @@ export async function executeJob(
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (job.sessionTarget === "none") {
|
||||||
|
if (job.payload.kind !== "directMessage") {
|
||||||
|
await finish("skipped", 'sessionTarget "none" requires payload.kind="directMessage"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = job.payload.text?.trim();
|
||||||
|
if (!text) {
|
||||||
|
await finish("skipped", "directMessage requires non-empty text");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.deps.sendDirectMessage) {
|
||||||
|
await finish("error", "directMessage not supported (sendDirectMessage not wired)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await state.deps.sendDirectMessage({
|
||||||
|
text,
|
||||||
|
channel: job.payload.channel ?? undefined,
|
||||||
|
to: job.payload.to ?? undefined,
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
await finish("ok", undefined, text);
|
||||||
|
} else {
|
||||||
|
await finish("error", res.error ?? "directMessage send failed");
|
||||||
|
}
|
||||||
|
} catch (sendErr) {
|
||||||
|
await finish("error", String(sendErr));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (job.sessionTarget === "main") {
|
if (job.sessionTarget === "main") {
|
||||||
const text = resolveJobPayloadTextForMain(job);
|
const text = resolveJobPayloadTextForMain(job);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export type CronSchedule =
|
|||||||
| { kind: "every"; everyMs: number; anchorMs?: number }
|
| { kind: "every"; everyMs: number; anchorMs?: number }
|
||||||
| { kind: "cron"; expr: string; tz?: string };
|
| { kind: "cron"; expr: string; tz?: string };
|
||||||
|
|
||||||
export type CronSessionTarget = "main" | "isolated";
|
export type CronSessionTarget = "main" | "isolated" | "none";
|
||||||
export type CronWakeMode = "next-heartbeat" | "now";
|
export type CronWakeMode = "next-heartbeat" | "now";
|
||||||
|
|
||||||
export type CronMessageChannel = ChannelId | "last";
|
export type CronMessageChannel = ChannelId | "last";
|
||||||
@ -24,7 +24,8 @@ export type CronPayload =
|
|||||||
channel?: CronMessageChannel;
|
channel?: CronMessageChannel;
|
||||||
to?: string;
|
to?: string;
|
||||||
bestEffortDeliver?: boolean;
|
bestEffortDeliver?: boolean;
|
||||||
};
|
}
|
||||||
|
| { kind: "directMessage"; text: string; channel?: CronMessageChannel; to?: string };
|
||||||
|
|
||||||
export type CronPayloadPatch =
|
export type CronPayloadPatch =
|
||||||
| { kind: "systemEvent"; text?: string }
|
| { kind: "systemEvent"; text?: string }
|
||||||
@ -39,7 +40,8 @@ export type CronPayloadPatch =
|
|||||||
channel?: CronMessageChannel;
|
channel?: CronMessageChannel;
|
||||||
to?: string;
|
to?: string;
|
||||||
bestEffortDeliver?: boolean;
|
bestEffortDeliver?: boolean;
|
||||||
};
|
}
|
||||||
|
| { kind: "directMessage"; text?: string; channel?: CronMessageChannel; to?: string };
|
||||||
|
|
||||||
export type CronIsolation = {
|
export type CronIsolation = {
|
||||||
postToMainPrefix?: string;
|
postToMainPrefix?: string;
|
||||||
|
|||||||
@ -50,6 +50,15 @@ export const CronPayloadSchema = Type.Union([
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
|
Type.Object(
|
||||||
|
{
|
||||||
|
kind: Type.Literal("directMessage"),
|
||||||
|
text: NonEmptyString,
|
||||||
|
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
|
||||||
|
to: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const CronPayloadPatchSchema = Type.Union([
|
export const CronPayloadPatchSchema = Type.Union([
|
||||||
@ -74,6 +83,15 @@ export const CronPayloadPatchSchema = Type.Union([
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
|
Type.Object(
|
||||||
|
{
|
||||||
|
kind: Type.Literal("directMessage"),
|
||||||
|
text: Type.Optional(NonEmptyString),
|
||||||
|
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
|
||||||
|
to: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const CronIsolationSchema = Type.Object(
|
export const CronIsolationSchema = Type.Object(
|
||||||
@ -110,7 +128,7 @@ export const CronJobSchema = Type.Object(
|
|||||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||||
updatedAtMs: Type.Integer({ minimum: 0 }),
|
updatedAtMs: Type.Integer({ minimum: 0 }),
|
||||||
schedule: CronScheduleSchema,
|
schedule: CronScheduleSchema,
|
||||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]),
|
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")]),
|
||||||
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
||||||
payload: CronPayloadSchema,
|
payload: CronPayloadSchema,
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
@ -136,7 +154,7 @@ export const CronAddParamsSchema = Type.Object(
|
|||||||
enabled: Type.Optional(Type.Boolean()),
|
enabled: Type.Optional(Type.Boolean()),
|
||||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||||
schedule: CronScheduleSchema,
|
schedule: CronScheduleSchema,
|
||||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]),
|
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")]),
|
||||||
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
||||||
payload: CronPayloadSchema,
|
payload: CronPayloadSchema,
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
@ -152,7 +170,7 @@ export const CronJobPatchSchema = Type.Object(
|
|||||||
enabled: Type.Optional(Type.Boolean()),
|
enabled: Type.Optional(Type.Boolean()),
|
||||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||||
schedule: Type.Optional(CronScheduleSchema),
|
schedule: Type.Optional(CronScheduleSchema),
|
||||||
sessionTarget: Type.Optional(Type.Union([Type.Literal("main"), Type.Literal("isolated")])),
|
sessionTarget: Type.Optional(Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")])),
|
||||||
wakeMode: Type.Optional(Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")])),
|
wakeMode: Type.Optional(Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")])),
|
||||||
payload: Type.Optional(CronPayloadPatchSchema),
|
payload: Type.Optional(CronPayloadPatchSchema),
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { CronService } from "../cron/service.js";
|
|||||||
import { resolveCronStorePath } from "../cron/store.js";
|
import { resolveCronStorePath } from "../cron/store.js";
|
||||||
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
||||||
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
||||||
|
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
import { normalizeAgentId } from "../routing/session-key.js";
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
@ -75,6 +76,27 @@ export function buildGatewayCronService(params: {
|
|||||||
lane: "cron",
|
lane: "cron",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
sendDirectMessage: async ({ text, channel, to }) => {
|
||||||
|
const runtimeConfig = loadConfig();
|
||||||
|
try {
|
||||||
|
await runMessageAction({
|
||||||
|
cfg: runtimeConfig,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
message: text,
|
||||||
|
...(channel ? { channel } : {}),
|
||||||
|
...(to ? { target: to } : {}),
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
clientName: "gateway-client",
|
||||||
|
mode: "backend",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: String(err) };
|
||||||
|
}
|
||||||
|
},
|
||||||
log: getChildLogger({ module: "cron", storePath }),
|
log: getChildLogger({ module: "cron", storePath }),
|
||||||
onEvent: (evt) => {
|
onEvent: (evt) => {
|
||||||
params.broadcast("cron", evt, { dropIfSlow: true });
|
params.broadcast("cron", evt, { dropIfSlow: true });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user