mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 14:00:42 +00:00
fix(acp): implicit streamToParent for mode=run without thread (#42404)
* fix(acp): implicit streamToParent for mode=run without thread When spawning ACP sessions with mode=run and no thread binding, automatically route output to parent session instead of Discord. This enables agent-to-agent supervision patterns where the spawning agent wants results returned programmatically, not posted as chat. The change makes sessions_spawn with runtime=acp and thread=false behave like direct acpx invocation - output goes to the spawning session, not to Discord. Fixes the issue where mode=run without thread still posted to Discord because hasDeliveryTarget was true when called from a Discord context. * fix: use resolved spawnMode instead of params.mode Move implicit streamToParent check to after resolveSpawnMode so that both explicit mode="run" and omitted mode (which defaults to "run" when thread is false) correctly trigger parent routing. This fixes the issue where callers that rely on default mode selection would not get the intended parent streaming behavior. * fix: tighten implicit ACP parent relay gating (#42404) (thanks @davidguttman) --------- Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ describe("heartbeat-reason", () => {
|
||||
expect(resolveHeartbeatReasonKind("manual")).toBe("manual");
|
||||
expect(resolveHeartbeatReasonKind("exec-event")).toBe("exec-event");
|
||||
expect(resolveHeartbeatReasonKind("wake")).toBe("wake");
|
||||
expect(resolveHeartbeatReasonKind("acp:spawn:stream")).toBe("wake");
|
||||
expect(resolveHeartbeatReasonKind("cron:job-1")).toBe("cron");
|
||||
expect(resolveHeartbeatReasonKind("hook:wake")).toBe("hook");
|
||||
expect(resolveHeartbeatReasonKind(" hook:wake ")).toBe("hook");
|
||||
@@ -35,6 +36,7 @@ describe("heartbeat-reason", () => {
|
||||
expect(isHeartbeatEventDrivenReason("exec-event")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("cron:job-1")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("wake")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("acp:spawn:stream")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("hook:gmail:sync")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("interval")).toBe(false);
|
||||
expect(isHeartbeatEventDrivenReason("manual")).toBe(false);
|
||||
|
||||
@@ -34,6 +34,9 @@ export function resolveHeartbeatReasonKind(reason?: string): HeartbeatReasonKind
|
||||
if (trimmed === "wake") {
|
||||
return "wake";
|
||||
}
|
||||
if (trimmed.startsWith("acp:spawn:")) {
|
||||
return "wake";
|
||||
}
|
||||
if (trimmed.startsWith("cron:")) {
|
||||
return "cron";
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@ import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getQueueSize } from "../process/command-queue.js";
|
||||
import { CommandLane } from "../process/lanes.js";
|
||||
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
toAgentStoreSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { escapeRegExp } from "../utils.js";
|
||||
import { formatErrorMessage, hasErrnoCode } from "./errors.js";
|
||||
@@ -53,9 +57,11 @@ import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js"
|
||||
import { resolveHeartbeatReasonKind } from "./heartbeat-reason.js";
|
||||
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
|
||||
import {
|
||||
areHeartbeatsEnabled,
|
||||
type HeartbeatRunResult,
|
||||
type HeartbeatWakeHandler,
|
||||
requestHeartbeatNow,
|
||||
setHeartbeatsEnabled,
|
||||
setHeartbeatWakeHandler,
|
||||
} from "./heartbeat-wake.js";
|
||||
import type { OutboundSendDeps } from "./outbound/deliver.js";
|
||||
@@ -75,11 +81,8 @@ export type HeartbeatDeps = OutboundSendDeps &
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("gateway/heartbeat");
|
||||
let heartbeatsEnabled = true;
|
||||
|
||||
export function setHeartbeatsEnabled(enabled: boolean) {
|
||||
heartbeatsEnabled = enabled;
|
||||
}
|
||||
export { areHeartbeatsEnabled, setHeartbeatsEnabled };
|
||||
|
||||
type HeartbeatConfig = AgentDefaultsConfig["heartbeat"];
|
||||
type HeartbeatAgent = {
|
||||
@@ -611,9 +614,14 @@ export async function runHeartbeatOnce(opts: {
|
||||
deps?: HeartbeatDeps;
|
||||
}): Promise<HeartbeatRunResult> {
|
||||
const cfg = opts.cfg ?? loadConfig();
|
||||
const agentId = normalizeAgentId(opts.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const explicitAgentId = typeof opts.agentId === "string" ? opts.agentId.trim() : "";
|
||||
const forcedSessionAgentId =
|
||||
explicitAgentId.length > 0 ? undefined : parseAgentSessionKey(opts.sessionKey)?.agentId;
|
||||
const agentId = normalizeAgentId(
|
||||
explicitAgentId || forcedSessionAgentId || resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const heartbeat = opts.heartbeat ?? resolveHeartbeatConfig(cfg, agentId);
|
||||
if (!heartbeatsEnabled) {
|
||||
if (!areHeartbeatsEnabled()) {
|
||||
return { status: "skipped", reason: "disabled" };
|
||||
}
|
||||
if (!isHeartbeatEnabledForAgent(cfg, agentId)) {
|
||||
@@ -1114,7 +1122,7 @@ export function startHeartbeatRunner(opts: {
|
||||
reason: "disabled",
|
||||
} satisfies HeartbeatRunResult;
|
||||
}
|
||||
if (!heartbeatsEnabled) {
|
||||
if (!areHeartbeatsEnabled()) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "disabled",
|
||||
|
||||
@@ -15,6 +15,16 @@ export type HeartbeatWakeHandler = (opts: {
|
||||
sessionKey?: string;
|
||||
}) => Promise<HeartbeatRunResult>;
|
||||
|
||||
let heartbeatsEnabled = true;
|
||||
|
||||
export function setHeartbeatsEnabled(enabled: boolean) {
|
||||
heartbeatsEnabled = enabled;
|
||||
}
|
||||
|
||||
export function areHeartbeatsEnabled(): boolean {
|
||||
return heartbeatsEnabled;
|
||||
}
|
||||
|
||||
type WakeTimerKind = "normal" | "retry";
|
||||
type PendingWakeReason = {
|
||||
reason: string;
|
||||
|
||||
Reference in New Issue
Block a user