fix(acp): reuse progress commentary config

This commit is contained in:
Ayaan Zaidi
2026-06-04 10:37:51 +00:00
parent 9ac94568f3
commit 2bf886b7dd
10 changed files with 264 additions and 48 deletions

View File

@@ -1178,7 +1178,6 @@ Notes:
hiddenBoundarySeparator: "paragraph", // none | space | newline | paragraph
maxOutputChars: 50000,
maxSessionUpdateChars: 500,
assistantCommentary: false,
},
runtime: {
@@ -1202,7 +1201,6 @@ Notes:
- `stream.hiddenBoundarySeparator`: separator before visible text after hidden tool events (default: `"paragraph"`).
- `stream.maxOutputChars`: maximum assistant output characters projected per ACP turn.
- `stream.maxSessionUpdateChars`: maximum characters for projected ACP status/update lines.
- `stream.assistantCommentary`: when `true`, relay assistant commentary and ACP status progress allowed by `stream.tagVisibility` into ACP parent stream updates. Defaults to `false`.
- `stream.tagVisibility`: record of tag names to boolean visibility overrides for streamed events.
- `runtime.ttlMinutes`: idle TTL in minutes for ACP session workers before eligible cleanup.
- `runtime.installCommand`: optional install command to run when bootstrapping an ACP runtime environment.

View File

@@ -548,10 +548,9 @@ Two ways to start an ACP session:
requester session as system events. Accepted responses include
`streamLogPath` pointing to a session-scoped JSONL log
(`<sessionId>.acp-stream.jsonl`) you can tail for full relay history.
Assistant commentary and ACP status progress text are hidden by default; set
`acp.stream.assistantCommentary: true` to include commentary in parent stream
updates while keeping final-answer delivery unchanged. Status progress still
honors `acp.stream.tagVisibility`, so tags such as `plan` remain hidden
Assistant commentary and ACP status progress text are shown for progress-mode
parent channels unless `streaming.progress.commentary=false`. Status progress
still honors `acp.stream.tagVisibility`, so tags such as `plan` remain hidden
unless explicitly enabled.
</ParamField>

View File

@@ -1,5 +1,6 @@
/** Tests ACP child-to-parent stream relay notices, routing, and log path resolution. */
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js";
const enqueueSystemEventMock = vi.fn();
@@ -48,10 +49,29 @@ vi.mock("../config/sessions/paths.js", async () => {
});
let emitAgentEvent: typeof import("../infra/agent-events.js").emitAgentEvent;
let resolveAcpProjectionSettings: typeof import("../auto-reply/reply/acp-stream-settings.js").resolveAcpProjectionSettings;
let resolveAcpSpawnStreamLogPath: typeof import("./acp-spawn-parent-stream.js").resolveAcpSpawnStreamLogPath;
let startAcpSpawnParentStreamRelay: typeof import("./acp-spawn-parent-stream.js").startAcpSpawnParentStreamRelay;
const progressCommentaryDeliveryContext = {
channel: "forum",
to: "-1001234567890",
accountId: "default",
threadId: 1122,
};
function progressModeConfig(acp?: OpenClawConfig["acp"]): OpenClawConfig {
return {
...(acp ? { acp } : {}),
channels: {
forum: {
streaming: {
mode: "progress",
},
},
},
};
}
function collectedTexts() {
return enqueueSystemEventMock.mock.calls.map((call) =>
typeof call[0] === "string" ? call[0] : (JSON.stringify(call[0]) ?? ""),
@@ -80,7 +100,6 @@ function firstMockCall(
describe("startAcpSpawnParentStreamRelay", () => {
beforeAll(async () => {
({ emitAgentEvent } = await import("../infra/agent-events.js"));
({ resolveAcpProjectionSettings } = await import("../auto-reply/reply/acp-stream-settings.js"));
({ resolveAcpSpawnStreamLogPath, startAcpSpawnParentStreamRelay } =
await import("./acp-spawn-parent-stream.js"));
});
@@ -487,19 +506,20 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("relays commentary-phase assistant text when enabled", () => {
it("relays commentary-phase assistant text in parent progress mode by default", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-commentary-enabled",
runId: "run-commentary-default",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-commentary-enabled",
childSessionKey: "agent:codex:acp:child-commentary-default",
agentId: "codex",
cfg: progressModeConfig(),
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
assistantCommentary: true,
});
emitAgentEvent({
runId: "run-commentary-enabled",
runId: "run-commentary-default",
stream: "assistant",
data: {
delta: "checking thread context; then post a tight progress reply here.",
@@ -516,24 +536,149 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("relays ACP status progress when assistant commentary and tag visibility are enabled", () => {
it("suppresses commentary-phase assistant text when parent progress commentary is disabled", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-commentary-disabled",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-commentary-disabled",
agentId: "codex",
cfg: {
channels: {
forum: {
streaming: {
mode: "progress",
progress: {
commentary: false,
},
},
},
},
},
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
});
emitAgentEvent({
runId: "run-commentary-disabled",
stream: "assistant",
data: {
delta: "checking thread context; then post a tight progress reply here.",
phase: "commentary",
},
});
vi.advanceTimersByTime(15);
expectNoTextWithFragment(collectedTexts(), "checking thread context");
relay.dispose();
});
it("inherits parent channel progress mode for account commentary overrides", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-account-commentary-enabled",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-account-commentary-enabled",
agentId: "codex",
cfg: {
channels: {
forum: {
streaming: {
mode: "progress",
},
accounts: {
work: {
streaming: {
progress: {
commentary: true,
},
},
},
},
},
},
},
deliveryContext: {
...progressCommentaryDeliveryContext,
accountId: "work",
},
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
});
emitAgentEvent({
runId: "run-account-commentary-enabled",
stream: "assistant",
data: {
delta: "checking account-scoped progress config.",
phase: "commentary",
},
});
vi.advanceTimersByTime(15);
expectTextWithFragment(collectedTexts(), "codex: checking account-scoped progress config.");
relay.dispose();
});
it("inherits legacy parent channel progress mode for account commentary overrides", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-account-legacy-commentary-enabled",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-account-legacy-commentary-enabled",
agentId: "codex",
cfg: {
channels: {
forum: {
streaming: "progress",
accounts: {
work: {
streaming: {
progress: {
commentary: true,
},
},
},
},
},
},
},
deliveryContext: {
...progressCommentaryDeliveryContext,
accountId: "work",
},
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
});
emitAgentEvent({
runId: "run-account-legacy-commentary-enabled",
stream: "assistant",
data: {
delta: "checking legacy progress config.",
phase: "commentary",
},
});
vi.advanceTimersByTime(15);
expectTextWithFragment(collectedTexts(), "codex: checking legacy progress config.");
relay.dispose();
});
it("relays ACP status progress when progress commentary and tag visibility are enabled", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-status-commentary-enabled",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-status-commentary-enabled",
agentId: "codex",
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
assistantCommentary: true,
acpProjectionSettings: resolveAcpProjectionSettings({
acp: {
stream: {
tagVisibility: {
plan: true,
},
cfg: progressModeConfig({
stream: {
tagVisibility: {
plan: true,
},
},
}),
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
});
emitAgentEvent({
@@ -552,15 +697,16 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("does not relay hidden ACP status tags when assistant commentary is enabled", () => {
it("does not relay hidden ACP status tags when progress commentary is enabled", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-status-commentary-hidden",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-status-commentary-hidden",
agentId: "codex",
cfg: progressModeConfig(),
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
assistantCommentary: true,
});
emitAgentEvent({
@@ -591,15 +737,16 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("does not relay ACP status tags hidden by default when assistant commentary is enabled", () => {
it("does not relay ACP status tags hidden by default when progress commentary is enabled", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-status-commentary-default-hidden",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-status-commentary-default-hidden",
agentId: "codex",
cfg: progressModeConfig(),
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 10,
noOutputNoticeMs: 120_000,
assistantCommentary: true,
});
emitAgentEvent({
@@ -624,10 +771,11 @@ describe("startAcpSpawnParentStreamRelay", () => {
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-commentary-visible-stall",
agentId: "codex",
cfg: progressModeConfig(),
deliveryContext: progressCommentaryDeliveryContext,
streamFlushMs: 1,
noOutputNoticeMs: 1_000,
noOutputPollMs: 250,
assistantCommentary: true,
});
emitAgentEvent({

View File

@@ -9,7 +9,12 @@ import {
resolveAcpProjectionSettings,
type AcpProjectionSettings,
} from "../auto-reply/reply/acp-stream-settings.js";
import {
resolveChannelStreamingProgressCommentary,
type StreamingCompatEntry,
} from "../channels/streaming.js";
import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { onAgentEvent } from "../infra/agent-events.js";
import {
type EventSessionRoutingPolicy,
@@ -19,6 +24,8 @@ import {
import { requestHeartbeat } from "../infra/heartbeat-wake.js";
import { appendRegularFile } from "../infra/regular-file.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import { normalizeAssistantPhase } from "../shared/chat-message-content.js";
import { recordTaskRunProgressByRunId } from "../tasks/detached-task-runtime.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
@@ -30,6 +37,10 @@ const DEFAULT_MAX_RELAY_LIFETIME_MS = 6 * 60 * 60 * 1000;
const STREAM_BUFFER_MAX_CHARS = 4_000;
const STREAM_SNIPPET_MAX_CHARS = 220;
type AcpParentProgressStreamingConfig = StreamingCompatEntry & {
accounts?: Record<string, StreamingCompatEntry | undefined>;
};
function compactWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
@@ -58,6 +69,75 @@ function formatProxyEnvSummary(keys: string[]): string {
return `proxy env: ${keys.join(", ")}`;
}
function asObjectRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function mergeStreamingConfig(base: unknown, override: unknown): unknown {
const baseRecord =
asObjectRecord(base) ?? (typeof base === "string" ? { mode: base } : undefined);
const overrideRecord = asObjectRecord(override);
if (!baseRecord || !overrideRecord) {
return override ?? base;
}
return {
...baseRecord,
...overrideRecord,
progress: {
...(asObjectRecord(baseRecord.progress) ?? {}),
...(asObjectRecord(overrideRecord.progress) ?? {}),
},
};
}
function mergeStreamingEntry(
base: AcpParentProgressStreamingConfig,
override: StreamingCompatEntry | undefined,
): StreamingCompatEntry {
if (!override) {
return base;
}
return {
...base,
...override,
streaming: mergeStreamingConfig(base.streaming, override.streaming),
};
}
function resolveParentProgressStreamingEntry(params: {
cfg: OpenClawConfig | undefined;
deliveryContext: DeliveryContext | undefined;
}): StreamingCompatEntry | undefined {
const channelId = normalizeOptionalString(params.deliveryContext?.channel);
if (!params.cfg || !channelId) {
return undefined;
}
const channels = params.cfg.channels as
| Record<string, AcpParentProgressStreamingConfig | undefined>
| undefined;
const channelCfg = channels?.[channelId];
if (!channelCfg) {
return undefined;
}
const accountCfg = resolveAccountEntry(
channelCfg.accounts,
normalizeAccountId(params.deliveryContext?.accountId),
);
return mergeStreamingEntry(channelCfg, accountCfg);
}
function resolveParentProgressCommentary(params: {
cfg: OpenClawConfig | undefined;
deliveryContext: DeliveryContext | undefined;
}): boolean {
return resolveChannelStreamingProgressCommentary(
resolveParentProgressStreamingEntry(params),
true,
);
}
function shouldRelayAcpStatusProgress(params: {
eventType: string | undefined;
tag: string | undefined;
@@ -134,8 +214,7 @@ export function startAcpSpawnParentStreamRelay(params: {
noOutputPollMs?: number;
maxRelayLifetimeMs?: number;
emitStartNotice?: boolean;
assistantCommentary?: boolean;
acpProjectionSettings?: AcpProjectionSettings;
cfg?: OpenClawConfig;
}): AcpSpawnParentRelayHandle {
const runId = normalizeOptionalString(params.runId) ?? "";
const parentSessionKey = normalizeOptionalString(params.parentSessionKey) ?? "";
@@ -228,8 +307,11 @@ export function startAcpSpawnParentStreamRelay(params: {
});
};
const shouldSurfaceUpdates = params.surfaceUpdates !== false;
const shouldRelayAssistantCommentary = params.assistantCommentary === true;
const acpProjectionSettings = params.acpProjectionSettings ?? resolveAcpProjectionSettings({});
const shouldRelayProgressCommentary = resolveParentProgressCommentary({
cfg: params.cfg,
deliveryContext: params.deliveryContext,
});
const acpProjectionSettings = resolveAcpProjectionSettings(params.cfg ?? {});
const eventRouting = params.eventRouting ?? {
mainKey: params.mainKey,
sessionScope: params.sessionScope,
@@ -445,7 +527,7 @@ export function startAcpSpawnParentStreamRelay(params: {
...(assistantPhase ? { phase: assistantPhase } : {}),
});
if (assistantPhase === "commentary" && !shouldRelayAssistantCommentary) {
if (assistantPhase === "commentary" && !shouldRelayProgressCommentary) {
lastProgressAt = Date.now();
return;
}
@@ -481,7 +563,7 @@ export function startAcpSpawnParentStreamRelay(params: {
firstRuntimeEventAt ??= Date.now();
lastRuntimeEventType = eventType;
if (
shouldRelayAssistantCommentary &&
shouldRelayProgressCommentary &&
shouldRelayAcpStatusProgress({
eventType,
tag,

View File

@@ -19,7 +19,6 @@ import {
import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../acp/policy.js";
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
import { DEFAULT_HEARTBEAT_EVERY } from "../auto-reply/heartbeat.js";
import { resolveAcpProjectionSettings } from "../auto-reply/reply/acp-stream-settings.js";
import { formatThinkingLevels } from "../auto-reply/thinking.js";
import {
resolveChannelDefaultBindingPlacement,
@@ -1552,8 +1551,6 @@ export async function spawnAcpDirect(
const parentEventRouting = parentSessionKey
? resolveEventSessionRoutingPolicy({ cfg, sessionKey: parentSessionKey })
: undefined;
const assistantCommentary = cfg.acp?.stream?.assistantCommentary === true;
const acpProjectionSettings = resolveAcpProjectionSettings(cfg);
if (effectiveStreamToParent && parentSessionKey) {
// Register relay before dispatch so fast lifecycle failures are not missed.
parentRelay = startAcpSpawnParentStreamRelay({
@@ -1567,8 +1564,7 @@ export async function spawnAcpDirect(
logPath: streamLogPath,
deliveryContext: parentDeliveryCtx,
emitStartNotice: false,
assistantCommentary,
acpProjectionSettings,
cfg,
});
}
const gatewayAttachments = toGatewayImageAttachments(params.attachments);
@@ -1628,8 +1624,7 @@ export async function spawnAcpDirect(
logPath: streamLogPath,
deliveryContext: parentDeliveryCtx,
emitStartNotice: false,
assistantCommentary,
acpProjectionSettings,
cfg,
});
}
parentRelay?.notifyStarted();

View File

@@ -153,7 +153,7 @@ export function isAcpTagVisible(settings: AcpProjectionSettings, tag: string | u
return override;
}
if (Object.hasOwn(ACP_TAG_VISIBILITY_DEFAULTS, tag)) {
return ACP_TAG_VISIBILITY_DEFAULTS[tag];
return ACP_TAG_VISIBILITY_DEFAULTS[tag as AcpSessionUpdateTag];
}
return true;
}

View File

@@ -220,8 +220,6 @@ export const FIELD_HELP: Record<string, string> = {
"Maximum assistant output characters projected per ACP turn before truncation notice is emitted.",
"acp.stream.maxSessionUpdateChars":
"Maximum characters for projected ACP session/update lines (tool/status updates).",
"acp.stream.assistantCommentary":
"When true, relay assistant commentary and ACP status progress allowed by acp.stream.tagVisibility into ACP parent stream updates. Defaults off.",
"acp.stream.tagVisibility":
"Per-sessionUpdate visibility overrides for ACP projection (for example usage_update, available_commands_update).",
"acp.runtime.ttlMinutes":

View File

@@ -560,7 +560,6 @@ export const FIELD_LABELS: Record<string, string> = {
"acp.stream.hiddenBoundarySeparator": "ACP Stream Hidden Boundary Separator",
"acp.stream.maxOutputChars": "ACP Stream Max Output Chars",
"acp.stream.maxSessionUpdateChars": "ACP Stream Max Session Update Chars",
"acp.stream.assistantCommentary": "ACP Stream Assistant Commentary",
"acp.stream.tagVisibility": "ACP Stream Tag Visibility",
"acp.runtime.ttlMinutes": "ACP Runtime TTL (minutes)",
"acp.runtime.installCommand": "ACP Runtime Install Command",

View File

@@ -21,8 +21,6 @@ export type AcpStreamConfig = {
maxOutputChars?: number;
/** Maximum visible characters for projected session/update lines. */
maxSessionUpdateChars?: number;
/** Relay assistant commentary/progress text into ACP parent stream updates. */
assistantCommentary?: boolean;
/**
* Per-sessionUpdate visibility overrides.
* Keys not listed here fall back to OpenClaw defaults.

View File

@@ -778,7 +778,6 @@ export const OpenClawSchema = z
.optional(),
maxOutputChars: z.number().int().positive().optional(),
maxSessionUpdateChars: z.number().int().positive().optional(),
assistantCommentary: z.boolean().optional(),
tagVisibility: z.record(z.string(), z.boolean()).optional(),
})
.strict()