plugin-runtime: expose runHeartbeatOnce in system API (#40299)

* plugin-runtime: expose runHeartbeatOnce in system API

Plugins that enqueue system events and need the agent to deliver
responses to the originating channel currently have no way to
override the default `heartbeat.target: "none"` behaviour.

Expose `runHeartbeatOnce` in the plugin runtime `system` namespace
so plugins can trigger a single heartbeat cycle with an explicit
`heartbeat: { target: "last" }` override — the same pattern the
cron service already uses (see #28508).

Changes:
- Add `RunHeartbeatOnceOptions` type and `runHeartbeatOnce` to
  `PluginRuntimeCore.system` (types-core.ts)
- Wire the function through a thin wrapper in runtime-system.ts
- Update the test-utils plugin-runtime mock

Made-with: Cursor

* feat(plugins): expose runHeartbeatOnce in system API (#40299) (thanks @loveyana)

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
This commit is contained in:
M1a0
2026-03-26 01:47:01 +08:00
committed by GitHub
parent 4ae4d1fabe
commit 7847e67f8a
6 changed files with 37 additions and 2 deletions

View File

@@ -1,13 +1,25 @@
import { runHeartbeatOnce as runHeartbeatOnceInternal } from "../../infra/heartbeat-runner.js";
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { runCommandWithTimeout } from "../../process/exec.js";
import { formatNativeDependencyHint } from "./native-deps.js";
import type { RunHeartbeatOnceOptions } from "./types-core.js";
import type { PluginRuntime } from "./types.js";
export function createRuntimeSystem(): PluginRuntime["system"] {
return {
enqueueSystemEvent,
requestHeartbeatNow,
runHeartbeatOnce: (opts?: RunHeartbeatOnceOptions) => {
// Destructure to forward only the plugin-safe subset; prevent cfg/deps injection at runtime.
const { reason, agentId, sessionKey, heartbeat } = opts ?? {};
return runHeartbeatOnceInternal({
reason,
agentId,
sessionKey,
heartbeat: heartbeat ? { target: heartbeat.target } : undefined,
});
},
runCommandWithTimeout,
formatNativeDependencyHint,
};

View File

@@ -1,5 +1,8 @@
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
import type { LogLevel } from "../../logging/levels.js";
export type { HeartbeatRunResult };
/** Structured logger surface injected into runtime-backed plugin helpers. */
export type RuntimeLogger = {
debug?: (message: string, meta?: Record<string, unknown>) => void;
@@ -8,6 +11,14 @@ export type RuntimeLogger = {
error: (message: string, meta?: Record<string, unknown>) => void;
};
export type RunHeartbeatOnceOptions = {
reason?: string;
agentId?: string;
sessionKey?: string;
/** Override heartbeat config (e.g. `{ target: "last" }` to deliver to the last active channel). */
heartbeat?: { target?: string };
};
/** Core runtime helpers exposed to trusted native plugins. */
export type PluginRuntimeCore = {
version: string;
@@ -37,6 +48,13 @@ export type PluginRuntimeCore = {
system: {
enqueueSystemEvent: typeof import("../../infra/system-events.js").enqueueSystemEvent;
requestHeartbeatNow: typeof import("../../infra/heartbeat-wake.js").requestHeartbeatNow;
/**
* Run a single heartbeat cycle immediately (bypassing the coalesce timer).
* Accepts an optional `heartbeat` config override so callers can force
* delivery to the last active channel — the same pattern the cron service
* uses to avoid the default `target: "none"` suppression.
*/
runHeartbeatOnce: (opts?: RunHeartbeatOnceOptions) => Promise<HeartbeatRunResult>;
runCommandWithTimeout: typeof import("../../process/exec.js").runCommandWithTimeout;
formatNativeDependencyHint: typeof import("./native-deps.js").formatNativeDependencyHint;
};