mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
feat(plugin-sdk): expose sessionTarget and agentId on cron_changed hook events (#77641)
This commit is contained in:
@@ -554,6 +554,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Auto-reply/queue: treat reset-triggered `/new` and `/reset` turns as interrupt runs across active-run queue handling, so steer/followup modes cannot delay a fresh session behind existing work. Fixes #74093. (#74144) Thanks @ruji9527 and @yelog.
|
||||
- Cron: persist repaired startup runtime state back to `jobs-state.json` so a valid future `nextRunAtMs` with missing `updatedAtMs` no longer triggers repeated external health-check repairs after Gateway restart. Fixes #76461. Thanks @vincentkoc.
|
||||
- Cron: preserve manual `cron.run` IDs in `cron.runs` history so manual run acknowledgements can be correlated with finished run records. Fixes #76276.
|
||||
- Plugin SDK/cron: expose `sessionTarget` and `agentId` as top-level fields on `cron_changed` hook events so downstream plugins can route cron completion results without digging into the optional job snapshot. Thanks @amknight.
|
||||
- CLI/devices: request `operator.admin` for `openclaw devices approve <requestId>` only when the exact pending device request would mint or inherit admin-scoped operator access, while keeping lower-scope approvals on the pairing scope.
|
||||
- Memory/embedding: broaden the embedding reindex retry classifier to include transient socket-layer errors (`fetch failed`, `ECONNRESET`, `socket hang up`, `UND_ERR_*`, `closed`) so memory reindex survives provider network hiccups instead of aborting mid-run. Related #56815, #44166. (#76311) Thanks @buyitsydney.
|
||||
- Memory/sessions: keep rotated and deleted transcripts (`.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>`) searchable by indexing archive content, mapping archive hits back to live transcript stems, emitting transcript update events on archive rotation, and bypassing incremental delta thresholds for one-shot archive mutations while keeping backups and compaction checkpoints opaque. Refs #56131. Thanks @buyitsydney.
|
||||
|
||||
@@ -150,8 +150,10 @@ describe("buildGatewayCronService", () => {
|
||||
expect.objectContaining({
|
||||
action: "added",
|
||||
jobId: job.id,
|
||||
sessionTarget: "main",
|
||||
job: expect.objectContaining({
|
||||
id: job.id,
|
||||
sessionTarget: "main",
|
||||
state: expect.objectContaining({ nextRunAtMs: job.state.nextRunAtMs }),
|
||||
}),
|
||||
}),
|
||||
@@ -191,9 +193,11 @@ describe("buildGatewayCronService", () => {
|
||||
expect.objectContaining({
|
||||
action: "removed",
|
||||
jobId: job.id,
|
||||
sessionTarget: "main",
|
||||
job: expect.objectContaining({
|
||||
id: job.id,
|
||||
name: "to-be-removed",
|
||||
sessionTarget: "main",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -205,6 +209,47 @@ describe("buildGatewayCronService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("cron_changed hook event includes agentId from the job", async () => {
|
||||
const cfg = createCronConfig("server-cron-hook-agentId");
|
||||
loadConfigMock.mockReturnValue(cfg);
|
||||
|
||||
const state = buildGatewayCronService({
|
||||
cfg,
|
||||
deps: {} as CliDeps,
|
||||
broadcast: () => {},
|
||||
});
|
||||
try {
|
||||
const job = await state.cron.add({
|
||||
name: "agent-scoped-job",
|
||||
enabled: true,
|
||||
agentId: "yinze",
|
||||
schedule: { kind: "every", everyMs: 60_000, anchorMs: 1_000 },
|
||||
sessionTarget: "session:project-alpha",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "agentTurn", message: "agent check" },
|
||||
});
|
||||
|
||||
expect(runCronChangedMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "added",
|
||||
jobId: job.id,
|
||||
sessionTarget: "session:project-alpha",
|
||||
agentId: "yinze",
|
||||
job: expect.objectContaining({
|
||||
id: job.id,
|
||||
agentId: "yinze",
|
||||
sessionTarget: "session:project-alpha",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
config: cfg,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
state.cron.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("cron_changed hook context uses runtime config from getRuntimeConfig()", async () => {
|
||||
const startupCfg = createCronConfig("server-cron-hook-runtime-cfg");
|
||||
const runtimeCfg = { ...startupCfg, _marker: "runtime" };
|
||||
|
||||
@@ -64,6 +64,7 @@ function pickDefined<T extends Record<string, unknown>>(
|
||||
function toPluginCronJob(job: CronJob): PluginHookGatewayCronJob {
|
||||
return {
|
||||
id: job.id,
|
||||
agentId: job.agentId,
|
||||
name: job.name,
|
||||
description: job.description,
|
||||
enabled: job.enabled,
|
||||
@@ -357,10 +358,18 @@ export function buildGatewayCronService(params: {
|
||||
// getJob() would return undefined. `delivery` and `usage` are
|
||||
// intentionally omitted — they contain internal channel/token detail
|
||||
// that is not part of the public plugin SDK surface.
|
||||
// Resolve job snapshot from the event or live service so top-level
|
||||
// convenience fields (sessionTarget, agentId) are always populated
|
||||
// when the job is known.
|
||||
const jobSnapshot = evt.job ?? cron.getJob(evt.jobId);
|
||||
const pluginJob = jobSnapshot ? toPluginCronJob(jobSnapshot) : undefined;
|
||||
const hookEvt: PluginHookCronChangedEvent = {
|
||||
action: evt.action,
|
||||
jobId: evt.jobId,
|
||||
...(evt.job ? { job: toPluginCronJob(evt.job) } : {}),
|
||||
...(pluginJob ? { job: pluginJob } : {}),
|
||||
// Top-level routing fields so plugins don't have to dig into job.
|
||||
sessionTarget: jobSnapshot?.sessionTarget,
|
||||
agentId: jobSnapshot?.agentId,
|
||||
...pickDefined(evt, [
|
||||
"runAtMs",
|
||||
"durationMs",
|
||||
|
||||
@@ -628,6 +628,8 @@ export type PluginHookGatewayCronJobState = {
|
||||
|
||||
export type PluginHookGatewayCronJob = {
|
||||
id: string;
|
||||
/** Agent id that owns this cron job. */
|
||||
agentId?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
@@ -662,6 +664,10 @@ export type PluginHookCronChangedEvent = {
|
||||
action: "added" | "updated" | "removed" | "started" | "finished";
|
||||
jobId: string;
|
||||
job?: PluginHookGatewayCronJob;
|
||||
/** Top-level session target for downstream routing (mirrors job.sessionTarget). */
|
||||
sessionTarget?: string;
|
||||
/** Agent id that owns this cron job (mirrors job.agentId). */
|
||||
agentId?: string;
|
||||
runAtMs?: number;
|
||||
durationMs?: number;
|
||||
status?: PluginHookGatewayCronRunStatus;
|
||||
|
||||
@@ -61,8 +61,12 @@ describe("gateway hook runner methods", () => {
|
||||
action: "updated",
|
||||
jobId: "job-1",
|
||||
nextRunAtMs: 123,
|
||||
sessionTarget: "main",
|
||||
agentId: "main",
|
||||
job: {
|
||||
id: "job-1",
|
||||
agentId: "main",
|
||||
sessionTarget: "main",
|
||||
state: { nextRunAtMs: 123 },
|
||||
},
|
||||
};
|
||||
@@ -78,6 +82,8 @@ describe("gateway hook runner methods", () => {
|
||||
const event: PluginHookCronChangedEvent = {
|
||||
action: "finished",
|
||||
jobId: "job-2",
|
||||
sessionTarget: "session:ops",
|
||||
agentId: "reporter",
|
||||
status: "error",
|
||||
error: "timeout",
|
||||
summary: "Job timed out",
|
||||
@@ -91,6 +97,8 @@ describe("gateway hook runner methods", () => {
|
||||
provider: "openai",
|
||||
job: {
|
||||
id: "job-2",
|
||||
agentId: "reporter",
|
||||
sessionTarget: "session:ops",
|
||||
state: { lastRunStatus: "error", lastError: "timeout" },
|
||||
},
|
||||
};
|
||||
@@ -106,13 +114,18 @@ describe("gateway hook runner methods", () => {
|
||||
const event: PluginHookCronChangedEvent = {
|
||||
action: "removed",
|
||||
jobId: "job-3",
|
||||
job: { id: "job-3", name: "deleted-job" },
|
||||
sessionTarget: "isolated",
|
||||
job: { id: "job-3", name: "deleted-job", sessionTarget: "isolated" },
|
||||
};
|
||||
|
||||
await runner.runCronChanged(event, gatewayCtx);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(event, gatewayCtx);
|
||||
expect(handler.mock.calls[0][0].job).toEqual({ id: "job-3", name: "deleted-job" });
|
||||
expect(handler.mock.calls[0][0].job).toEqual({
|
||||
id: "job-3",
|
||||
name: "deleted-job",
|
||||
sessionTarget: "isolated",
|
||||
});
|
||||
});
|
||||
|
||||
it("hasHooks returns true for registered gateway hooks", () => {
|
||||
|
||||
Reference in New Issue
Block a user