mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-30 12:53:31 +00:00
fix(cron): scope agent wake targets (#97949)
* fix(cron): scope agent wake targets * fix(cron): refresh prompt snapshots
This commit is contained in:
@@ -555,35 +555,39 @@ describe("cron tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("derives agentId from an explicit cross-agent sessionKey instead of the caller's agentId", async () => {
|
||||
// A caller in agent-123 explicitly waking an agent-456 session must
|
||||
// NOT have agent-123's agentId paired with agent-456's sessionKey —
|
||||
// that would canonicalize back to agent-123's main lane on the
|
||||
// gateway side.
|
||||
it("rejects an explicit cross-agent sessionKey", async () => {
|
||||
const tool = createTestCronTool({
|
||||
agentSessionKey: "agent:agent-123:telegram:direct:channing",
|
||||
});
|
||||
await tool.execute("call-wake-cross-agent", {
|
||||
action: "wake",
|
||||
text: "follow up",
|
||||
sessionKey: "agent:agent-456:discord:thread-xyz",
|
||||
});
|
||||
const params = expectSingleGatewayCallMethod("wake");
|
||||
expect(params).toEqual({
|
||||
mode: "next-heartbeat",
|
||||
text: "follow up",
|
||||
sessionKey: "agent:agent-456:discord:thread-xyz",
|
||||
agentId: "agent-456",
|
||||
await expect(
|
||||
tool.execute("call-wake-cross-agent", {
|
||||
action: "wake",
|
||||
text: "follow up",
|
||||
sessionKey: "agent:agent-456:discord:thread-xyz",
|
||||
}),
|
||||
).rejects.toThrow("cron sessionKey must match the calling agent");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects an explicit cross-agent agentId", async () => {
|
||||
const tool = createTestCronTool({
|
||||
agentSessionKey: "agent:agent-123:telegram:direct:channing",
|
||||
});
|
||||
await expect(
|
||||
tool.execute("call-wake-cross-agent-id", {
|
||||
action: "wake",
|
||||
text: "follow up",
|
||||
agentId: "agent-456",
|
||||
}),
|
||||
).rejects.toThrow("wake agentId must match the calling agent");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a contradictory explicit agentId + agent-prefixed sessionKey pair", async () => {
|
||||
// The gateway target resolver treats agentId as authoritative, so a
|
||||
// contradictory pair would silently canonicalize the wake onto a session
|
||||
// the caller never named. The tool rejects instead of guessing.
|
||||
const tool = createTestCronTool({
|
||||
agentSessionKey: "agent:agent-123:telegram:direct:channing",
|
||||
});
|
||||
const tool = createTestCronTool();
|
||||
await expect(
|
||||
tool.execute("call-wake-explicit-pair", {
|
||||
action: "wake",
|
||||
@@ -595,29 +599,26 @@ describe("cron tool", () => {
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts an explicit agentId that matches the agent owning the explicit sessionKey", async () => {
|
||||
it("accepts a different session owned by the calling agent", async () => {
|
||||
const tool = createTestCronTool({
|
||||
agentSessionKey: "agent:agent-123:telegram:direct:channing",
|
||||
});
|
||||
await tool.execute("call-wake-matching-pair", {
|
||||
action: "wake",
|
||||
text: "manual",
|
||||
sessionKey: "agent:agent-456:discord:thread-xyz",
|
||||
agentId: "agent-456",
|
||||
sessionKey: "agent:agent-123:discord:thread-xyz",
|
||||
agentId: "agent-123",
|
||||
});
|
||||
const params = expectSingleGatewayCallMethod("wake");
|
||||
expect(params).toEqual({
|
||||
mode: "next-heartbeat",
|
||||
text: "manual",
|
||||
sessionKey: "agent:agent-456:discord:thread-xyz",
|
||||
agentId: "agent-456",
|
||||
sessionKey: "agent:agent-123:discord:thread-xyz",
|
||||
agentId: "agent-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits agentId when explicit sessionKey is not in agent:<id>:* form and no explicit agentId is given", async () => {
|
||||
// Defence-in-depth: if the explicit sessionKey can't be parsed for an
|
||||
// agentId, we'd rather omit it (gateway falls back to default routing
|
||||
// for that session) than incorrectly attach the caller's agentId.
|
||||
it("binds an unparseable explicit sessionKey to the calling agent", async () => {
|
||||
const tool = createTestCronTool({
|
||||
agentSessionKey: "agent:agent-123:telegram:direct:channing",
|
||||
});
|
||||
@@ -631,9 +632,7 @@ describe("cron tool", () => {
|
||||
mode: "next-heartbeat",
|
||||
text: "x",
|
||||
sessionKey: "subagent:weird:format",
|
||||
// No agentId — explicit sessionKey wasn't parseable + no explicit
|
||||
// override, so we deliberately drop agentId rather than inherit
|
||||
// the caller's.
|
||||
agentId: "agent-123",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ export function createCronToolSchema(): TSchema {
|
||||
sessionKey: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
'Wake target override for `action: "wake"`: route the event to the named session rather than the calling agent\'s current session. Defaults to the resolved calling-session key when omitted.',
|
||||
'Wake target override for `action: "wake"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.',
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -858,7 +858,7 @@ ACTIONS:
|
||||
- remove: delete job; needs jobId
|
||||
- run: run only if due by default; needs jobId; pass runMode="force" to trigger now
|
||||
- runs: run history; needs jobId
|
||||
- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane.
|
||||
- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane owned by the calling agent.
|
||||
|
||||
JOB SCHEMA (for add action):
|
||||
{
|
||||
@@ -1226,12 +1226,20 @@ Use jobId canonical; id accepted compat. contextMessages (0-10) adds previous me
|
||||
// upstream half of openclaw/openclaw#46886 (#64556 — agentId/
|
||||
// sessionKey silently ignored for `action: "wake"`). Explicit
|
||||
// params on the tool call still take precedence over the inferred
|
||||
// value, so call sites that want to wake a different session can
|
||||
// pass `sessionKey` / `agentId` directly.
|
||||
// value, so call sites can wake a different session owned by the
|
||||
// calling agent.
|
||||
const cfg = getRuntimeConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const explicitSessionKey = readStringParam(params, "sessionKey");
|
||||
const explicitAgentId = readStringParam(params, "agentId");
|
||||
if (callerScope) {
|
||||
assertCronToolAgentFieldMatchesScope({
|
||||
value: explicitAgentId,
|
||||
field: "wake agentId",
|
||||
callerScope,
|
||||
});
|
||||
assertCronToolSessionRefsMatchScope({ sessionKey: explicitSessionKey }, callerScope);
|
||||
}
|
||||
const inferredSessionKey = opts?.agentSessionKey
|
||||
? resolveInternalSessionKey({ key: opts.agentSessionKey, alias, mainKey })
|
||||
: undefined;
|
||||
@@ -1266,6 +1274,7 @@ Use jobId canonical; id accepted compat. contextMessages (0-10) adds previous me
|
||||
);
|
||||
}
|
||||
const agentId =
|
||||
callerScope?.agentId ??
|
||||
explicitAgentId ??
|
||||
(explicitSessionKey ? agentIdFromExplicitSessionKey : inferredAgentId);
|
||||
return jsonResult(
|
||||
|
||||
@@ -346,6 +346,21 @@ describe("gateway tool defaults", () => {
|
||||
expect(call.agentRuntimeIdentityToken).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("marks local wake calls from trusted tool context with agent runtime identity", async () => {
|
||||
mocks.callGateway.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
await withGatewayToolCallerIdentity(
|
||||
{ agentId: "ops", sessionKey: "agent:ops:telegram:direct:alice" },
|
||||
async () => {
|
||||
await callGatewayTool("wake", {}, { mode: "now", text: "ping" });
|
||||
},
|
||||
);
|
||||
|
||||
const call = capturedGatewayCall();
|
||||
expect(call.method).toBe("wake");
|
||||
expect(call.agentRuntimeIdentityToken).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("explains stale gateway cron connection metadata rejections", async () => {
|
||||
mocks.callGateway.mockRejectedValueOnce(
|
||||
new Error(
|
||||
|
||||
@@ -211,6 +211,7 @@ const APPROVAL_RUNTIME_METHODS = new Set<string>([
|
||||
]);
|
||||
|
||||
const AGENT_RUNTIME_IDENTITY_METHODS = new Set<string>([
|
||||
"wake",
|
||||
"cron.list",
|
||||
"cron.get",
|
||||
"cron.add",
|
||||
|
||||
@@ -473,7 +473,7 @@ function isCronInvalidRequestError(err: unknown): boolean {
|
||||
|
||||
/** Gateway request handlers for cron jobs and cron run-log access. */
|
||||
export const cronHandlers: GatewayRequestHandlers = {
|
||||
wake: ({ params, respond, context }) => {
|
||||
wake: ({ params, respond, context, client }) => {
|
||||
if (!validateWakeParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
@@ -516,6 +516,27 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
const sessionKeyAgentId = sessionKey
|
||||
? parseAgentSessionKey(sessionKey)?.agentId?.trim().toLowerCase()
|
||||
: undefined;
|
||||
const callerScope = readCronCallerScope(client);
|
||||
if (callerScope && agentId && normalizeAgentId(agentId) !== callerScope.agentId) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "wake agentId outside caller scope"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
callerScope &&
|
||||
sessionKeyAgentId &&
|
||||
normalizeAgentId(sessionKeyAgentId) !== callerScope.agentId
|
||||
) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "wake sessionKey outside caller scope"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (agentId && sessionKeyAgentId && agentId.toLowerCase() !== sessionKeyAgentId) {
|
||||
respond(
|
||||
false,
|
||||
@@ -531,7 +552,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
mode: p.mode,
|
||||
text: p.text,
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
...(agentId ? { agentId } : {}),
|
||||
...(callerScope ? { agentId: callerScope.agentId } : agentId ? { agentId } : {}),
|
||||
});
|
||||
respond(true, result, undefined);
|
||||
},
|
||||
|
||||
@@ -193,8 +193,8 @@ async function invokeCronRemove(
|
||||
return await invokeCron("cron.remove", params, { context, client: options?.client });
|
||||
}
|
||||
|
||||
async function invokeWake(params: Record<string, unknown>) {
|
||||
return await invokeCron("wake", params);
|
||||
async function invokeWake(params: Record<string, unknown>, client?: GatewayClient) {
|
||||
return await invokeCron("wake", params, { client });
|
||||
}
|
||||
|
||||
function createCronJob(overrides: Partial<CronJob> = {}): CronJob {
|
||||
@@ -1576,6 +1576,44 @@ describe("cron method validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "agentId",
|
||||
params: { agentId: "agent-456" },
|
||||
message: "wake agentId outside caller scope",
|
||||
},
|
||||
{
|
||||
name: "sessionKey",
|
||||
params: { sessionKey: "agent:agent-456:discord:thread-xyz" },
|
||||
message: "wake sessionKey outside caller scope",
|
||||
},
|
||||
])("rejects a cross-agent $name for agent-runtime callers", async ({ params, message }) => {
|
||||
const { context, respond } = await invokeWake(
|
||||
{ mode: "now", text: "ping", ...params },
|
||||
callerClient("agent-123"),
|
||||
);
|
||||
expect(context.cron.wake).not.toHaveBeenCalled();
|
||||
expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: message });
|
||||
});
|
||||
|
||||
it("binds agent-runtime wake calls to the calling agent", async () => {
|
||||
const { context, respond } = await invokeWake(
|
||||
{
|
||||
mode: "now",
|
||||
text: "ping",
|
||||
sessionKey: "agent:agent-123:discord:thread-xyz",
|
||||
},
|
||||
callerClient("agent-123"),
|
||||
);
|
||||
expect(context.cron.wake).toHaveBeenCalledWith({
|
||||
mode: "now",
|
||||
text: "ping",
|
||||
sessionKey: "agent:agent-123:discord:thread-xyz",
|
||||
agentId: "agent-123",
|
||||
});
|
||||
expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
});
|
||||
|
||||
it("treats whitespace-only sessionKey as omitted at the handler boundary", async () => {
|
||||
const { context, respond } = await invokeWake({
|
||||
mode: "now",
|
||||
|
||||
@@ -293,7 +293,7 @@
|
||||
},
|
||||
{
|
||||
"deferLoading": true,
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane owned by the calling agent.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"inputSchema": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -890,7 +890,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sessionKey": {
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to the named session rather than the calling agent's current session. Defaults to the resolved calling-session key when omitted.",
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
|
||||
@@ -293,7 +293,7 @@
|
||||
},
|
||||
{
|
||||
"deferLoading": true,
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane owned by the calling agent.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"inputSchema": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -890,7 +890,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sessionKey": {
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to the named session rather than the calling agent's current session. Defaults to the resolved calling-session key when omitted.",
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
|
||||
@@ -293,7 +293,7 @@
|
||||
},
|
||||
{
|
||||
"deferLoading": true,
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"description": "Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.\n\nMain cron => system events for heartbeat. Isolated cron => background task in `openclaw tasks`.\n\nACTIONS:\n- status: scheduler status\n- list: compact job summaries; includeDisabled true includes disabled; use get for full job details; agentId filter auto-filled from session\n- get: one job; needs jobId\n- add: create job; needs job object\n- update: patch job; needs jobId + patch\n- remove: delete job; needs jobId\n- run: run only if due by default; needs jobId; pass runMode=\"force\" to trigger now\n- runs: run history; needs jobId\n- wake: send wake event; needs text, optional mode; defaults the target to the calling session/agent. Pass top-level sessionKey/agentId to wake a different lane owned by the calling agent.\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string\",\n \"schedule\": { ... }, // required\n \"payload\": { ... }, // required\n \"delivery\": { ... }, // optional announce for isolated/current/session, webhook for any target\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<id>\",\n \"enabled\": true | false // default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": main session; requires payload.kind=\"systemEvent\"\n- \"isolated\": ephemeral isolated session; requires payload.kind=\"agentTurn\"\n- \"current\": bind current session at creation\n- \"session:<id>\": persistent named session\n\nDEFAULTS:\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nCurrent binding needs sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": one-shot absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": recurring interval\n { \"kind\": \"every\", \"everyMs\": <ms>, \"anchorMs\": <optional-ms> }\n- \"cron\": expr in supplied timezone, or Gateway host local timezone when tz omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in local wall-clock time; do not convert the requested local time to UTC first.\n tz omitted => Gateway host local timezone, not UTC.\n Example 6pm Shanghai daily: { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor \"at\", ISO timestamps without timezone are UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": inject text as system event\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": run agent with prompt; isolated/current/session only\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0=no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - isolated agentTurn default when omitted: \"announce\"\n - announce: send to chat channel; isolated/current/session only; optional channel/to\n - threadId: chat thread/topic id\n - webhook: POST finished-run event to delivery.to URL\n - Specific chat/recipient: set announce delivery.channel/to; do not call messaging tools inside run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- Webhook: delivery.mode=\"webhook\" and delivery.to URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" default: wake next heartbeat\n- \"now\": wake immediately\n\nUse jobId canonical; id accepted compat. contextMessages (0-10) adds previous messages as job context.",
|
||||
"inputSchema": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -890,7 +890,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sessionKey": {
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to the named session rather than the calling agent's current session. Defaults to the resolved calling-session key when omitted.",
|
||||
"description": "Wake target override for `action: \"wake\"`: route the event to another session owned by the calling agent. Defaults to the resolved calling-session key when omitted.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
|
||||
@@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 50947,
|
||||
"roughTokens": 12737
|
||||
"chars": 50951,
|
||||
"roughTokens": 12738
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 2994,
|
||||
@@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6927
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 78655,
|
||||
"roughTokens": 19664
|
||||
"chars": 78659,
|
||||
"roughTokens": 19665
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 1629,
|
||||
|
||||
@@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 50616,
|
||||
"roughTokens": 12654
|
||||
"chars": 50620,
|
||||
"roughTokens": 12655
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 1964,
|
||||
@@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6544
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 76794,
|
||||
"roughTokens": 19199
|
||||
"chars": 76798,
|
||||
"roughTokens": 19200
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 1129,
|
||||
|
||||
@@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 51906,
|
||||
"roughTokens": 12977
|
||||
"chars": 51910,
|
||||
"roughTokens": 12978
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 1983,
|
||||
@@ -240,8 +240,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6780
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 79027,
|
||||
"roughTokens": 19757
|
||||
"chars": 79031,
|
||||
"roughTokens": 19758
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 1367,
|
||||
|
||||
Reference in New Issue
Block a user