mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 17:22:56 +00:00
* refactor: extract agent core package Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts. * refactor: extract shared llm runtime Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout. * refactor: remove pi runtime internals Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code. * refactor: tighten agent session runtime Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts. * refactor: remove static model and pi auth paths Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities. * refactor: remove legacy provider compat paths * docs: remove agent parity notes * fix: skip provider wildcard metadata parsing * refactor: share session extension sdk loading * refactor: inline acpx proxy error formatter * refactor: fold edit recovery into edit tool * fix: accept extension batch separator * test: align startup provider plugin expectations * fix: restore provider-scoped release discovery * test: align static asset packaging expectations * fix: run static provider catalogs during scoped discovery * fix: add provider entry catalogs for scoped live discovery * fix: load lightweight provider catalog entries * fix: refresh provider-scoped plugin metadata * fix: keep provider catalog entries on release live path * fix: keep static manifest models in release live checks * fix: harden release model discovery * fix: reduce OpenAI live cache probe reasoning * fix: disable OpenAI cache probe reasoning * ci: extend OpenAI gateway live timeout * fix: extend live gateway model budget * fix: stabilize release validation regressions * fix: honor provider aliases in model rows * fix: stabilize release validation lanes * fix: stabilize release memory qa * ci: stabilize release validation lanes * ci: prefer ipv4 for live docker node calls * fix: restore shared tool-call stream wrapper * ci: remove legacy pi test shard alias * fix: clean up embedded agent test drift * fix: stabilize runtime alias status * fix: clean up embedded agent ci drift * fix: restore release ci invariants * fix: clean up post-rebase runtime drift * fix: restore release ci checks * fix: restore release ci after rebase * fix: remove stale pi runtime path * test: align compaction runtime expectations * test: update plugin prerelease expectations * fix: handle claude live tool approvals * fix: stabilize release validation gates * fix: finish agent runtime import * test: finish post-rebase agent runtime mocks * fix: keep codex compaction native * fix: stabilize codex app-server hook tests * test: isolate codex diagnostic active run * test: remove codex diagnostic completion race # Conflicts: # extensions/codex/src/app-server/run-attempt.test.ts * ci: fix full release manifest performance run id * refactor: narrow llm plugin sdk boundary * chore: drop generated google boundary stamps * fix: repair rebase fallout * fix: clean up rebased runtime references * fix: decode codex jwt payloads as base64url * fix: preserve shipped pi runtime alias * fix: add scoped sdk virtual modules * fix: decode llm codex oauth jwt as base64url * fix: avoid stale vertex adc negative cache * fix: harden tool arg decoding and codeql path * fix: keep vertex adc negative checks live * refactor: consolidate codex jwt and edit helpers * fix: await codex oauth node runtime imports * fix: preserve sdk tool and notice contracts * fix: preserve shipped compat config boundaries * fix: align codex oauth callback host * fix: terminate agent-core loop streams on failure * fix: keep codex oauth callback alive during fallback * ci: include session tools in critical codeql scans * fix: keep Cloudflare Anthropic provider auth header * docs: redirect legacy pi runtime pages * fix: honor bundled web provider compat discovery * fix: protect session output spill files * fix: keep legacy agent dir env blocked * fix: contain auto-discovered skill symlinks * fix: harden agent core sdk proxy surfaces * fix: restore approval reaction sdk compat * fix: keep live docker runs bounded * fix: keep codex oauth redirect host aligned * fix: resolve post-rebase agent runtime drift * fix: redact anthropic oauth parse failures * fix: preserve responses strict tool shaping * fix: repair agent runtime rebase cleanup * docs: redirect retired parity pages * fix: bound auto-discovered resources to roots * fix: repair post-rebase agent test drift * fix: preserve bundled provider allowlist migration * fix: preserve manifest-owned provider aliases * fix: declare photon image dependency * fix: keep provider headers out of proxy body * fix: preserve shipped env aliases * fix: refresh control ui i18n generated state * fix: quote read fallback paths * fix: preview edits through configured backend * test: satisfy core test typecheck * fix: preserve ZAI usage auth fallback * test: repair codex diagnostic test * fix: repair agent runtime rebase drift * test: finish embedded runner import rename * fix: repair agent runtime rebase integrations * test: align compaction oauth fallback expectations * fix: allow sdk-auth session models * fix: update doctor tool schema import * fix: preserve bedrock plugin region * fix: stream harmony-like prose immediately * ci: include session runtime in codeql shards * fix: repair latest rebase integrations * fix: honor explicit codex websocket transport * fix: keep openai-compatible credentials provider-scoped * fix: refresh sdk api baseline after rebase * fix: route cli runtime aliases through openclaw harness * test: rename stale harness mock expectation * test: rename embedded agent overflow calls * test: clean embedded auth test wording * test: use openclaw stream types in deepinfra cache test * fix: refresh sdk api baseline on latest main * fix: honor bundled discovery compat allowlists * fix: refresh sdk api baseline after latest rebase * fix: remove stale rebase imports * test: rename stale model catalog mock * test: mock renamed doctor runtime modules * fix: map canonical kimi env auth * fix: use internal model registry in bench script * fix: migrate deepinfra provider catalog entry * fix: enforce builtin tool suppression * fix: route compaction auth and proxy payloads safely * refactor: prune unused llm registry leftovers * test: update codex hooks session import * test: fix model picker ci coverage * test: align model picker auth mock types
513 lines
17 KiB
TypeScript
513 lines
17 KiB
TypeScript
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
import { abortAndDrainEmbeddedAgentRun } from "../agents/embedded-agent.js";
|
|
import { cleanupBrowserSessionsForLifecycleEnd } from "../browser-lifecycle-cleanup.js";
|
|
import type { CliDeps } from "../cli/deps.types.js";
|
|
import { getRuntimeConfig } from "../config/io.js";
|
|
import {
|
|
canonicalizeMainSessionAlias,
|
|
resolveAgentIdFromSessionKey,
|
|
resolveAgentMainSessionKey,
|
|
} from "../config/sessions.js";
|
|
import { resolveStorePath } from "../config/sessions/paths.js";
|
|
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js";
|
|
import {
|
|
appendCronRunLog,
|
|
resolveCronRunLogPath,
|
|
resolveCronRunLogPruneOptions,
|
|
} from "../cron/run-log.js";
|
|
import type { CronServiceContract } from "../cron/service-contract.js";
|
|
import { CronService } from "../cron/service.js";
|
|
import { resolveCronSessionTargetSessionKey } from "../cron/session-target.js";
|
|
import { resolveCronStorePath } from "../cron/store.js";
|
|
import type { CronJob } from "../cron/types.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import { resolveMainScopedEventSessionKey } from "../infra/event-session-routing.js";
|
|
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
|
import { requestHeartbeat } from "../infra/heartbeat-wake.js";
|
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
import { getChildLogger } from "../logging.js";
|
|
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
|
import type {
|
|
PluginHookCronChangedEvent,
|
|
PluginHookGatewayCronJob,
|
|
PluginHookGatewayCronService,
|
|
PluginHookGatewayContext,
|
|
} from "../plugins/hook-types.js";
|
|
import {
|
|
normalizeAgentId,
|
|
resolveEventSessionKey,
|
|
toAgentStoreSessionKey,
|
|
} from "../routing/session-key.js";
|
|
import { defaultRuntime } from "../runtime.js";
|
|
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
|
|
import {
|
|
dispatchGatewayCronFinishedNotifications,
|
|
sendGatewayCronFailureAlert,
|
|
} from "./server-cron-notifications.js";
|
|
|
|
export type GatewayCronState = {
|
|
cron: CronServiceContract;
|
|
storePath: string;
|
|
cronEnabled: boolean;
|
|
};
|
|
|
|
/** Pick only the keys whose values are not `undefined` from an object. */
|
|
function pickDefined<T extends Record<string, unknown>>(
|
|
obj: T,
|
|
keys: (keyof T)[],
|
|
): Partial<Pick<T, (typeof keys)[number]>> {
|
|
const result: Partial<Pick<T, (typeof keys)[number]>> = {};
|
|
for (const k of keys) {
|
|
if (obj[k] !== undefined) {
|
|
(result as Record<string, unknown>)[k as string] = obj[k];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function omitExplicitHeartbeatDestination(
|
|
heartbeat: AgentDefaultsConfig["heartbeat"] | undefined,
|
|
): AgentDefaultsConfig["heartbeat"] | undefined {
|
|
if (!heartbeat) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...heartbeat,
|
|
to: undefined,
|
|
accountId: undefined,
|
|
};
|
|
}
|
|
|
|
function sanitizeCronHeartbeatOverride(
|
|
heartbeat: AgentDefaultsConfig["heartbeat"] | undefined,
|
|
): AgentDefaultsConfig["heartbeat"] | undefined {
|
|
return heartbeat?.target === "last" ? omitExplicitHeartbeatDestination(heartbeat) : heartbeat;
|
|
}
|
|
|
|
/** Map internal CronJob to the public plugin SDK shape. */
|
|
function toPluginCronJob(job: CronJob): PluginHookGatewayCronJob {
|
|
return {
|
|
id: job.id,
|
|
agentId: job.agentId,
|
|
name: job.name,
|
|
description: job.description,
|
|
enabled: job.enabled,
|
|
schedule: job.schedule ? structuredClone(job.schedule) : undefined,
|
|
sessionTarget: job.sessionTarget,
|
|
wakeMode: job.wakeMode,
|
|
payload: job.payload ? structuredClone(job.payload) : undefined,
|
|
state: {
|
|
nextRunAtMs: job.state.nextRunAtMs,
|
|
runningAtMs: job.state.runningAtMs,
|
|
lastRunAtMs: job.state.lastRunAtMs,
|
|
lastRunStatus: job.state.lastRunStatus,
|
|
lastError: job.state.lastError,
|
|
lastDurationMs: job.state.lastDurationMs,
|
|
lastDelivered: job.state.lastDelivered,
|
|
lastDeliveryStatus: job.state.lastDeliveryStatus,
|
|
lastDeliveryError: job.state.lastDeliveryError,
|
|
lastFailureNotificationDelivered: job.state.lastFailureNotificationDelivered,
|
|
lastFailureNotificationDeliveryStatus: job.state.lastFailureNotificationDeliveryStatus,
|
|
lastFailureNotificationDeliveryError: job.state.lastFailureNotificationDeliveryError,
|
|
},
|
|
createdAtMs: job.createdAtMs,
|
|
updatedAtMs: job.updatedAtMs,
|
|
};
|
|
}
|
|
|
|
export function buildGatewayCronService(params: {
|
|
cfg: OpenClawConfig;
|
|
deps: CliDeps;
|
|
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
|
|
}): GatewayCronState {
|
|
const cronLogger = getChildLogger({ module: "cron" });
|
|
const storePath = resolveCronStorePath(params.cfg.cron?.store);
|
|
const cronEnabled = process.env.OPENCLAW_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false;
|
|
|
|
const findAgentEntry = (cfg: OpenClawConfig, agentId: string) =>
|
|
Array.isArray(cfg.agents?.list)
|
|
? cfg.agents.list.find(
|
|
(entry) =>
|
|
entry && typeof entry.id === "string" && normalizeAgentId(entry.id) === agentId,
|
|
)
|
|
: undefined;
|
|
|
|
const hasConfiguredAgent = (cfg: OpenClawConfig, agentId: string) =>
|
|
Boolean(findAgentEntry(cfg, agentId));
|
|
|
|
const mergeRuntimeAgentConfig = (runtimeConfig: OpenClawConfig, requestedAgentId: string) => {
|
|
if (hasConfiguredAgent(runtimeConfig, requestedAgentId)) {
|
|
return runtimeConfig;
|
|
}
|
|
const fallbackAgentEntry = findAgentEntry(params.cfg, requestedAgentId);
|
|
if (!fallbackAgentEntry) {
|
|
return runtimeConfig;
|
|
}
|
|
const startupAgents = params.cfg.agents;
|
|
const runtimeAgents = runtimeConfig.agents;
|
|
return {
|
|
...runtimeConfig,
|
|
agents: {
|
|
...startupAgents,
|
|
...runtimeAgents,
|
|
defaults: {
|
|
...startupAgents?.defaults,
|
|
...runtimeAgents?.defaults,
|
|
},
|
|
list: [...(runtimeAgents?.list ?? []), fallbackAgentEntry],
|
|
},
|
|
};
|
|
};
|
|
|
|
const resolveCronAgent = (requested?: string | null) => {
|
|
const runtimeConfig = getRuntimeConfig();
|
|
const normalized =
|
|
typeof requested === "string" && requested.trim() ? normalizeAgentId(requested) : undefined;
|
|
const effectiveConfig =
|
|
normalized !== undefined ? mergeRuntimeAgentConfig(runtimeConfig, normalized) : runtimeConfig;
|
|
const agentId =
|
|
normalized !== undefined && hasConfiguredAgent(effectiveConfig, normalized)
|
|
? normalized
|
|
: resolveDefaultAgentId(effectiveConfig);
|
|
return { agentId, cfg: effectiveConfig };
|
|
};
|
|
|
|
const resolveCronSessionKey = (params: {
|
|
runtimeConfig: OpenClawConfig;
|
|
agentId: string;
|
|
requestedSessionKey?: string | null;
|
|
}) => {
|
|
const requested = params.requestedSessionKey?.trim();
|
|
if (!requested) {
|
|
return resolveAgentMainSessionKey({
|
|
cfg: params.runtimeConfig,
|
|
agentId: params.agentId,
|
|
});
|
|
}
|
|
const candidate = toAgentStoreSessionKey({
|
|
agentId: params.agentId,
|
|
requestKey: requested,
|
|
mainKey: params.runtimeConfig.session?.mainKey,
|
|
});
|
|
const canonical = canonicalizeMainSessionAlias({
|
|
cfg: params.runtimeConfig,
|
|
agentId: params.agentId,
|
|
sessionKey: candidate,
|
|
});
|
|
if (canonical !== "global") {
|
|
const sessionAgentId = resolveAgentIdFromSessionKey(canonical);
|
|
if (normalizeAgentId(sessionAgentId) !== normalizeAgentId(params.agentId)) {
|
|
return resolveAgentMainSessionKey({
|
|
cfg: params.runtimeConfig,
|
|
agentId: params.agentId,
|
|
});
|
|
}
|
|
}
|
|
return (
|
|
resolveMainScopedEventSessionKey({
|
|
cfg: params.runtimeConfig,
|
|
sessionKey: canonical,
|
|
agentId: params.agentId,
|
|
}) ?? canonical
|
|
);
|
|
};
|
|
|
|
const resolveCronTarget = (opts?: {
|
|
agentId?: string | null;
|
|
sessionKey?: string | null;
|
|
preserveUntargeted?: boolean;
|
|
}) => {
|
|
const requestedAgentId =
|
|
typeof opts?.agentId === "string" && opts.agentId.trim()
|
|
? normalizeAgentId(opts.agentId)
|
|
: undefined;
|
|
const requestedSessionKey =
|
|
typeof opts?.sessionKey === "string" && opts.sessionKey.trim() ? opts.sessionKey : undefined;
|
|
if (opts?.preserveUntargeted && !requestedAgentId && !requestedSessionKey) {
|
|
return { runtimeConfig: getRuntimeConfig(), agentId: undefined, sessionKey: undefined };
|
|
}
|
|
|
|
// Derive from canonical agent-prefixed keys only. Relative keys intentionally
|
|
// fall through to the configured default instead of hardcoding "main".
|
|
const derivedAgentId =
|
|
requestedSessionKey && parseAgentSessionKey(requestedSessionKey)
|
|
? resolveAgentIdFromSessionKey(requestedSessionKey)
|
|
: undefined;
|
|
const { agentId: resolvedAgentId, cfg: runtimeConfig } = resolveCronAgent(
|
|
requestedAgentId ?? derivedAgentId,
|
|
);
|
|
const agentId = resolvedAgentId || undefined;
|
|
const resolvedSessionKey = agentId
|
|
? resolveCronSessionKey({
|
|
runtimeConfig,
|
|
agentId,
|
|
requestedSessionKey,
|
|
})
|
|
: undefined;
|
|
const sessionKey =
|
|
resolvedSessionKey && runtimeConfig.session?.scope === "global"
|
|
? resolveEventSessionKey(
|
|
resolvedSessionKey,
|
|
runtimeConfig.session?.mainKey,
|
|
runtimeConfig.session?.scope,
|
|
)
|
|
: resolvedSessionKey;
|
|
return { runtimeConfig, agentId, sessionKey };
|
|
};
|
|
|
|
const resolveCronHeartbeatOverride = (params: {
|
|
runtimeConfig: OpenClawConfig;
|
|
agentId?: string;
|
|
heartbeat?: AgentDefaultsConfig["heartbeat"];
|
|
}) => {
|
|
if (!params.heartbeat) {
|
|
return undefined;
|
|
}
|
|
const agentEntry =
|
|
params.agentId !== undefined
|
|
? findAgentEntry(params.runtimeConfig, params.agentId)
|
|
: undefined;
|
|
const agentHeartbeat =
|
|
agentEntry && typeof agentEntry === "object" ? agentEntry.heartbeat : undefined;
|
|
const baseHeartbeat = {
|
|
...params.runtimeConfig.agents?.defaults?.heartbeat,
|
|
...agentHeartbeat,
|
|
};
|
|
const heartbeatOverride = { ...baseHeartbeat, ...params.heartbeat };
|
|
return sanitizeCronHeartbeatOverride(heartbeatOverride);
|
|
};
|
|
|
|
const defaultAgentId = resolveDefaultAgentId(params.cfg);
|
|
const runLogPrune = resolveCronRunLogPruneOptions(params.cfg.cron?.runLog);
|
|
const resolveSessionStorePath = (agentId?: string) =>
|
|
resolveStorePath(params.cfg.session?.store, {
|
|
agentId: agentId ?? defaultAgentId,
|
|
});
|
|
const sessionStorePath = resolveSessionStorePath(defaultAgentId);
|
|
const warnedLegacyWebhookJobs = new Set<string>();
|
|
|
|
const runCronChangedHook = (evt: PluginHookCronChangedEvent) => {
|
|
const hookRunner = getGlobalHookRunner();
|
|
if (!hookRunner?.hasHooks("cron_changed")) {
|
|
return;
|
|
}
|
|
const hookCtx: PluginHookGatewayContext = {
|
|
config: getRuntimeConfig(),
|
|
getCron: () => cron as PluginHookGatewayCronService,
|
|
};
|
|
void hookRunner.runCronChanged(evt, hookCtx).catch((err) => {
|
|
cronLogger.warn(
|
|
{ err: formatErrorMessage(err), jobId: evt.jobId },
|
|
"cron_changed hook failed",
|
|
);
|
|
});
|
|
};
|
|
|
|
const cron = new CronService({
|
|
storePath,
|
|
cronEnabled,
|
|
cronConfig: params.cfg.cron,
|
|
defaultAgentId,
|
|
resolveSessionStorePath,
|
|
sessionStorePath,
|
|
enqueueSystemEvent: (text, opts) => {
|
|
const { sessionKey } = resolveCronTarget(opts);
|
|
if (!sessionKey) {
|
|
throw new Error("Cron system event target did not resolve a session key.");
|
|
}
|
|
enqueueSystemEvent(text, {
|
|
sessionKey,
|
|
contextKey: opts?.contextKey,
|
|
deliveryContext: opts?.deliveryContext,
|
|
});
|
|
},
|
|
requestHeartbeat: (opts) => {
|
|
const { agentId, sessionKey } = resolveCronTarget({ ...opts, preserveUntargeted: true });
|
|
requestHeartbeat({
|
|
source: opts?.source ?? "cron",
|
|
intent: opts?.intent ?? "event",
|
|
reason: opts?.reason,
|
|
agentId,
|
|
sessionKey,
|
|
heartbeat: sanitizeCronHeartbeatOverride(opts?.heartbeat),
|
|
});
|
|
},
|
|
runHeartbeatOnce: async (opts) => {
|
|
const { runtimeConfig, agentId, sessionKey } = resolveCronTarget({
|
|
...opts,
|
|
preserveUntargeted: true,
|
|
});
|
|
return await runHeartbeatOnce({
|
|
cfg: runtimeConfig,
|
|
source: opts?.source ?? "cron",
|
|
intent: opts?.intent ?? "event",
|
|
reason: opts?.reason,
|
|
agentId,
|
|
sessionKey,
|
|
heartbeat: resolveCronHeartbeatOverride({
|
|
runtimeConfig,
|
|
agentId,
|
|
heartbeat: opts?.heartbeat,
|
|
}),
|
|
deps: { ...params.deps, runtime: defaultRuntime },
|
|
});
|
|
},
|
|
runIsolatedAgentJob: async ({
|
|
job,
|
|
message,
|
|
abortSignal,
|
|
onExecutionStarted,
|
|
onExecutionPhase,
|
|
}) => {
|
|
const { agentId, cfg: runtimeConfig } = resolveCronAgent(job.agentId);
|
|
const sessionKey = resolveCronSessionTargetSessionKey(job.sessionTarget) ?? `cron:${job.id}`;
|
|
try {
|
|
return await runCronIsolatedAgentTurn({
|
|
cfg: runtimeConfig,
|
|
deps: params.deps,
|
|
job,
|
|
message,
|
|
abortSignal,
|
|
onExecutionStarted,
|
|
onExecutionPhase,
|
|
agentId,
|
|
sessionKey,
|
|
lane: "cron",
|
|
});
|
|
} finally {
|
|
await cleanupBrowserSessionsForLifecycleEnd({
|
|
sessionKeys: [sessionKey],
|
|
onWarn: (msg) => cronLogger.warn({ jobId: job.id }, msg),
|
|
});
|
|
}
|
|
},
|
|
cleanupTimedOutAgentRun: async ({ job, execution }) => {
|
|
if (!execution?.sessionId) {
|
|
return;
|
|
}
|
|
const result = await abortAndDrainEmbeddedAgentRun({
|
|
sessionId: execution.sessionId,
|
|
sessionKey: execution.sessionKey,
|
|
settleMs: 15_000,
|
|
forceClear: true,
|
|
reason: "cron_timeout",
|
|
});
|
|
cronLogger.warn(
|
|
{
|
|
jobId: job.id,
|
|
sessionId: execution.sessionId,
|
|
sessionKey: execution.sessionKey,
|
|
aborted: result.aborted,
|
|
drained: result.drained,
|
|
forceCleared: result.forceCleared,
|
|
},
|
|
"cron: cleaned up timed-out agent run",
|
|
);
|
|
},
|
|
sendCronFailureAlert: async ({ job, text, channel, to, mode, accountId }) =>
|
|
await sendGatewayCronFailureAlert({
|
|
deps: params.deps,
|
|
logger: cronLogger,
|
|
resolveCronAgent,
|
|
webhookToken: params.cfg.cron?.webhookToken,
|
|
job,
|
|
text,
|
|
channel,
|
|
to,
|
|
mode,
|
|
accountId,
|
|
}),
|
|
log: getChildLogger({ module: "cron", storePath }),
|
|
onEvent: (evt) => {
|
|
params.broadcast("cron", evt, { dropIfSlow: true });
|
|
// Build hook event from CronEvent. The job snapshot is carried on the
|
|
// internal event so it's available even for "removed" actions where
|
|
// 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,
|
|
...(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",
|
|
"status",
|
|
"error",
|
|
"summary",
|
|
"delivered",
|
|
"deliveryStatus",
|
|
"deliveryError",
|
|
"sessionId",
|
|
"sessionKey",
|
|
"runId",
|
|
"nextRunAtMs",
|
|
"model",
|
|
"provider",
|
|
]),
|
|
};
|
|
runCronChangedHook(hookEvt);
|
|
if (evt.action === "finished") {
|
|
const job = evt.job ?? cron.getJob(evt.jobId);
|
|
dispatchGatewayCronFinishedNotifications({
|
|
evt,
|
|
job,
|
|
deps: params.deps,
|
|
logger: cronLogger,
|
|
resolveCronAgent,
|
|
webhookToken: params.cfg.cron?.webhookToken,
|
|
legacyWebhook: params.cfg.cron?.webhook,
|
|
globalFailureDestination: params.cfg.cron?.failureDestination,
|
|
warnedLegacyWebhookJobs,
|
|
});
|
|
|
|
const logPath = resolveCronRunLogPath({
|
|
storePath,
|
|
jobId: evt.jobId,
|
|
});
|
|
void appendCronRunLog(
|
|
logPath,
|
|
{
|
|
ts: Date.now(),
|
|
jobId: evt.jobId,
|
|
action: "finished",
|
|
status: evt.status,
|
|
error: evt.error,
|
|
summary: evt.summary,
|
|
diagnostics: evt.diagnostics,
|
|
delivered: evt.delivered,
|
|
deliveryStatus: evt.deliveryStatus,
|
|
deliveryError: evt.deliveryError,
|
|
failureNotificationDelivery: evt.failureNotificationDelivery,
|
|
delivery: evt.delivery,
|
|
sessionId: evt.sessionId,
|
|
sessionKey: evt.sessionKey,
|
|
runId: evt.runId,
|
|
runAtMs: evt.runAtMs,
|
|
durationMs: evt.durationMs,
|
|
nextRunAtMs: evt.nextRunAtMs,
|
|
model: evt.model,
|
|
provider: evt.provider,
|
|
usage: evt.usage,
|
|
},
|
|
runLogPrune,
|
|
).catch((err) => {
|
|
cronLogger.warn({ err: String(err), logPath }, "cron: run log append failed");
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
return { cron, storePath, cronEnabled };
|
|
}
|