mirror of https://github.com/openclaw/openclaw.git
config: add toggle for empty-success exec exit notifications
This commit is contained in:
parent
bf054d0597
commit
005c7df908
|
|
@ -31,6 +31,7 @@ export interface ProcessSession {
|
|||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
notifyOnExit?: boolean;
|
||||
notifyOnExitEmptySuccess?: boolean;
|
||||
exitNotified?: boolean;
|
||||
child?: ChildProcessWithoutNullStreams;
|
||||
stdin?: SessionStdin;
|
||||
|
|
|
|||
|
|
@ -342,6 +342,29 @@ describe("exec notifyOnExit", () => {
|
|||
expect(status).toBe("completed");
|
||||
expect(peekSystemEvents("agent:main:main")).toEqual([]);
|
||||
});
|
||||
|
||||
it("can re-enable no-op completion events via notifyOnExitEmptySuccess", async () => {
|
||||
const tool = createExecTool({
|
||||
allowBackground: true,
|
||||
backgroundMs: 0,
|
||||
notifyOnExit: true,
|
||||
notifyOnExitEmptySuccess: true,
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
const result = await tool.execute("call3", {
|
||||
command: shortDelayCmd,
|
||||
background: true,
|
||||
});
|
||||
|
||||
expect(result.details.status).toBe("running");
|
||||
const sessionId = (result.details as { sessionId: string }).sessionId;
|
||||
const status = await waitForCompletion(sessionId);
|
||||
expect(status).toBe("completed");
|
||||
const events = peekSystemEvents("agent:main:main");
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
expect(events.some((event) => event.includes("Exec completed"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exec PATH handling", () => {
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ function maybeNotifyOnExit(session: ProcessSession, status: "completed" | "faile
|
|||
const output = compactNotifyOutput(
|
||||
tail(session.tail || session.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS),
|
||||
);
|
||||
if (status === "completed" && !output) {
|
||||
if (status === "completed" && !output && session.notifyOnExitEmptySuccess !== true) {
|
||||
return;
|
||||
}
|
||||
const summary = output
|
||||
|
|
@ -366,6 +366,7 @@ export async function runExecProcess(opts: {
|
|||
maxOutput: number;
|
||||
pendingMaxOutput: number;
|
||||
notifyOnExit: boolean;
|
||||
notifyOnExitEmptySuccess?: boolean;
|
||||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
timeoutSec: number;
|
||||
|
|
@ -531,6 +532,7 @@ export async function runExecProcess(opts: {
|
|||
scopeKey: opts.scopeKey,
|
||||
sessionKey: opts.sessionKey,
|
||||
notifyOnExit: opts.notifyOnExit,
|
||||
notifyOnExitEmptySuccess: opts.notifyOnExitEmptySuccess === true,
|
||||
exitNotified: false,
|
||||
child: child ?? undefined,
|
||||
stdin,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export type ExecToolDefaults = {
|
|||
sessionKey?: string;
|
||||
messageProvider?: string;
|
||||
notifyOnExit?: boolean;
|
||||
notifyOnExitEmptySuccess?: boolean;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
|
|
@ -135,6 +136,7 @@ export function createExecTool(
|
|||
const defaultPathPrepend = normalizePathPrepend(defaults?.pathPrepend);
|
||||
const safeBins = resolveSafeBins(defaults?.safeBins);
|
||||
const notifyOnExit = defaults?.notifyOnExit !== false;
|
||||
const notifyOnExitEmptySuccess = defaults?.notifyOnExitEmptySuccess === true;
|
||||
const notifySessionKey = defaults?.sessionKey?.trim() || undefined;
|
||||
const approvalRunningNoticeMs = resolveApprovalRunningNoticeMs(defaults?.approvalRunningNoticeMs);
|
||||
// Derive agentId only when sessionKey is an agent session key.
|
||||
|
|
@ -749,6 +751,7 @@ export function createExecTool(
|
|||
maxOutput,
|
||||
pendingMaxOutput,
|
||||
notifyOnExit: false,
|
||||
notifyOnExitEmptySuccess: false,
|
||||
scopeKey: defaults?.scopeKey,
|
||||
sessionKey: notifySessionKey,
|
||||
timeoutSec: effectiveTimeout,
|
||||
|
|
@ -883,6 +886,7 @@ export function createExecTool(
|
|||
maxOutput,
|
||||
pendingMaxOutput,
|
||||
notifyOnExit,
|
||||
notifyOnExitEmptySuccess,
|
||||
scopeKey: defaults?.scopeKey,
|
||||
sessionKey: notifySessionKey,
|
||||
timeoutSec: effectiveTimeout,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
|||
agentExec?.approvalRunningNoticeMs ?? globalExec?.approvalRunningNoticeMs,
|
||||
cleanupMs: agentExec?.cleanupMs ?? globalExec?.cleanupMs,
|
||||
notifyOnExit: agentExec?.notifyOnExit ?? globalExec?.notifyOnExit,
|
||||
notifyOnExitEmptySuccess:
|
||||
agentExec?.notifyOnExitEmptySuccess ?? globalExec?.notifyOnExitEmptySuccess,
|
||||
applyPatch: agentExec?.applyPatch ?? globalExec?.applyPatch,
|
||||
};
|
||||
}
|
||||
|
|
@ -329,6 +331,8 @@ export function createOpenClawCodingTools(options?: {
|
|||
approvalRunningNoticeMs:
|
||||
options?.exec?.approvalRunningNoticeMs ?? execConfig.approvalRunningNoticeMs,
|
||||
notifyOnExit: options?.exec?.notifyOnExit ?? execConfig.notifyOnExit,
|
||||
notifyOnExitEmptySuccess:
|
||||
options?.exec?.notifyOnExitEmptySuccess ?? execConfig.notifyOnExitEmptySuccess,
|
||||
sandbox: sandbox
|
||||
? {
|
||||
containerName: sandbox.containerName,
|
||||
|
|
|
|||
|
|
@ -331,12 +331,14 @@ export async function handleBashChatCommand(params: {
|
|||
const shouldBackgroundImmediately = foregroundMs <= 0;
|
||||
const timeoutSec = params.cfg.tools?.exec?.timeoutSec;
|
||||
const notifyOnExit = params.cfg.tools?.exec?.notifyOnExit;
|
||||
const notifyOnExitEmptySuccess = params.cfg.tools?.exec?.notifyOnExitEmptySuccess;
|
||||
const execTool = createExecTool({
|
||||
scopeKey: CHAT_BASH_SCOPE_KEY,
|
||||
allowBackground: true,
|
||||
timeoutSec,
|
||||
sessionKey: params.sessionKey,
|
||||
notifyOnExit,
|
||||
notifyOnExitEmptySuccess,
|
||||
elevated: {
|
||||
enabled: params.elevated.enabled,
|
||||
allowed: params.elevated.allowed,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ export const FIELD_HELP: Record<string, string> = {
|
|||
'Optional allowlist of model ids (e.g. "gpt-5.2" or "openai/gpt-5.2").',
|
||||
"tools.exec.notifyOnExit":
|
||||
"When true (default), backgrounded exec sessions enqueue a system event and request a heartbeat on exit.",
|
||||
"tools.exec.notifyOnExitEmptySuccess":
|
||||
"When true, successful backgrounded exec exits with empty output still enqueue a completion system event (default: false).",
|
||||
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
|
||||
"tools.exec.safeBins":
|
||||
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
|
||||
"tools.fs.workspaceOnly": "Workspace-only FS tools",
|
||||
"tools.exec.notifyOnExit": "Exec Notify On Exit",
|
||||
"tools.exec.notifyOnExitEmptySuccess": "Exec Notify On Empty Success",
|
||||
"tools.exec.approvalRunningNoticeMs": "Exec Approval Running Notice (ms)",
|
||||
"tools.exec.host": "Exec Host",
|
||||
"tools.exec.security": "Exec Security",
|
||||
|
|
|
|||
|
|
@ -183,6 +183,11 @@ export type ExecToolConfig = {
|
|||
cleanupMs?: number;
|
||||
/** Emit a system event and heartbeat when a backgrounded exec exits. */
|
||||
notifyOnExit?: boolean;
|
||||
/**
|
||||
* Also emit success exit notifications when a backgrounded exec has no output.
|
||||
* Default false to reduce context noise.
|
||||
*/
|
||||
notifyOnExitEmptySuccess?: boolean;
|
||||
/** apply_patch subtool configuration (experimental). */
|
||||
applyPatch?: {
|
||||
/** Enable apply_patch for OpenAI models (default: false). */
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ export const AgentToolsSchema = z
|
|||
approvalRunningNoticeMs: z.number().int().nonnegative().optional(),
|
||||
cleanupMs: z.number().int().positive().optional(),
|
||||
notifyOnExit: z.boolean().optional(),
|
||||
notifyOnExitEmptySuccess: z.boolean().optional(),
|
||||
applyPatch: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
|
@ -546,6 +547,7 @@ export const ToolsSchema = z
|
|||
timeoutSec: z.number().int().positive().optional(),
|
||||
cleanupMs: z.number().int().positive().optional(),
|
||||
notifyOnExit: z.boolean().optional(),
|
||||
notifyOnExitEmptySuccess: z.boolean().optional(),
|
||||
applyPatch: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue