mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-25 16:53:02 +08:00
Compare commits
1 Commits
v2026.6.10
...
codex/mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa840c26f |
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Install/update: reject OpenClaw GitHub source package targets early and point moving-main users at the dev/git install path instead of the broken npm source-install flow.
|
||||
- Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.
|
||||
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
|
||||
- Gateway/chat: surface message-tool-only room-event failures in chat diagnostics and session transcripts so suppressed source replies stay debuggable. Thanks @amknight.
|
||||
- Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.
|
||||
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
|
||||
- Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.
|
||||
|
||||
@@ -242,9 +242,15 @@ vi.mock("../logging/subsystem.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../routing/session-key.js", () => ({
|
||||
classifySessionKeyShape: (key?: string | null) => (key?.trim() ? "legacy_or_alias" : "missing"),
|
||||
isSubagentSessionKey: () => false,
|
||||
isUnscopedSessionKeySentinel: (key?: string | null) =>
|
||||
["global", "unknown"].includes(key?.trim().toLowerCase() ?? ""),
|
||||
normalizeAgentId: (id: string) => id,
|
||||
normalizeMainKey: (key?: string | null) => key?.trim() || "main",
|
||||
resolveAgentIdFromSessionKey: () => "main",
|
||||
scopeLegacySessionKeyToAgent: ({ sessionKey }: { sessionKey?: string | null }) =>
|
||||
sessionKey?.trim() || undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
|
||||
@@ -582,6 +582,8 @@ async function agentCommandInternal(
|
||||
const startedAt = Date.now();
|
||||
registerAgentRunContext(runId, {
|
||||
sessionKey,
|
||||
sourceReplyDeliveryMode: opts.sourceReplyDeliveryMode,
|
||||
suppressPromptPersistence: opts.suppressPromptPersistence === true,
|
||||
});
|
||||
attemptExecutionRuntime.emitAcpLifecycleStart({ runId, startedAt });
|
||||
|
||||
@@ -726,6 +728,8 @@ async function agentCommandInternal(
|
||||
registerAgentRunContext(runId, {
|
||||
sessionKey,
|
||||
verboseLevel: resolvedVerboseLevel,
|
||||
sourceReplyDeliveryMode: opts.sourceReplyDeliveryMode,
|
||||
suppressPromptPersistence: opts.suppressPromptPersistence === true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1309,6 +1309,9 @@ export async function runAgentTurnWithFallback(params: {
|
||||
verboseLevel: params.resolvedVerboseLevel,
|
||||
isHeartbeat: params.isHeartbeat,
|
||||
isControlUiVisible: shouldSurfaceToControlUi,
|
||||
sourceReplyDeliveryMode: params.followupRun.run.sourceReplyDeliveryMode,
|
||||
suppressPromptPersistence: params.followupRun.run.suppressNextUserMessagePersistence === true,
|
||||
inboundEventKind: params.followupRun.currentInboundEventKind,
|
||||
});
|
||||
}
|
||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||
|
||||
@@ -5443,6 +5443,63 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
|
||||
expect(dispatcher.sendToolResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mirrors suppressed message-tool-only room event failures into the transcript", async () => {
|
||||
setNoAbort();
|
||||
sessionStoreMocks.currentEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: 0,
|
||||
sendPolicy: "allow",
|
||||
};
|
||||
const dispatcher = createDispatcher();
|
||||
const failureReply = {
|
||||
text: "agent failed before sending through message",
|
||||
isError: true,
|
||||
} satisfies ReplyPayload;
|
||||
const replyResolver = vi.fn(async () => failureReply);
|
||||
const ctx = buildTestCtx({
|
||||
ChatType: "group",
|
||||
InboundEventKind: "room_event",
|
||||
MessageSid: "telegram-msg-1",
|
||||
SessionKey: "test:session",
|
||||
});
|
||||
|
||||
const result = await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg: emptyConfig,
|
||||
dispatcher,
|
||||
replyResolver,
|
||||
replyOptions: {
|
||||
runId: "run-1",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
},
|
||||
});
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(result.queuedFinal).toBe(false);
|
||||
expect(result.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
expect(transcriptMocks.appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith({
|
||||
sessionKey: "test:session",
|
||||
agentId: "main",
|
||||
text: expect.stringContaining(
|
||||
"Message-tool-only room event failed before a visible source reply was delivered.",
|
||||
),
|
||||
idempotencyKey: "run-1:message-tool-only-failure-diagnostic",
|
||||
updateMode: "inline",
|
||||
config: emptyConfig,
|
||||
});
|
||||
const transcriptText = (
|
||||
transcriptMocks.appendAssistantMessageToSessionTranscript.mock.calls[0]?.[0] as {
|
||||
text?: string;
|
||||
}
|
||||
)?.text;
|
||||
expect(transcriptText).toContain("The inbound prompt was not saved to chat history by design");
|
||||
expect(transcriptText).toContain(
|
||||
"Suppressed error: agent failed before sending through message",
|
||||
);
|
||||
expect(transcriptText).toContain("Diagnostics: inbound=room_event, mode=message_tool_only.");
|
||||
});
|
||||
|
||||
it("mirrors internal source reply payloads into the active transcript", async () => {
|
||||
setNoAbort();
|
||||
sessionStoreMocks.currentEntry = {
|
||||
|
||||
@@ -2137,11 +2137,53 @@ export async function dispatchReplyFromConfig(
|
||||
let routedFinalCount = 0;
|
||||
let attemptedFinalDelivery = false;
|
||||
let finalDeliveryFailed = false;
|
||||
let mirroredSuppressedSourceReplyFailure = false;
|
||||
const shouldDeliverDespiteSourceReplySuppression = (reply: ReplyPayload) =>
|
||||
suppressAutomaticSourceDelivery &&
|
||||
ctx.InboundEventKind !== "room_event" &&
|
||||
!sendPolicyDenied &&
|
||||
getReplyPayloadMetadata(reply)?.deliverDespiteSourceReplySuppression === true;
|
||||
const mirrorSuppressedSourceReplyFailureToTranscript = async (reply: ReplyPayload) => {
|
||||
const transcriptSessionKey = acpDispatchSessionKey ?? sessionKey;
|
||||
if (
|
||||
mirroredSuppressedSourceReplyFailure ||
|
||||
!transcriptSessionKey ||
|
||||
sourceReplyDeliveryMode !== "message_tool_only" ||
|
||||
ctx.InboundEventKind !== "room_event" ||
|
||||
reply.isError !== true ||
|
||||
sendPolicyDenied
|
||||
) {
|
||||
return;
|
||||
}
|
||||
mirroredSuppressedSourceReplyFailure = true;
|
||||
const replyText = reply.text?.trim();
|
||||
const diagnosticText = [
|
||||
"OpenClaw diagnostic: Message-tool-only room event failed before a visible source reply was delivered.",
|
||||
"The inbound prompt was not saved to chat history by design, and automatic room fallback delivery was suppressed.",
|
||||
"A visible reply for this turn must be sent through the `message` tool in the originating client.",
|
||||
replyText ? `Suppressed error: ${replyText}` : "",
|
||||
"Diagnostics: inbound=room_event, mode=message_tool_only.",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const idempotencySource =
|
||||
params.replyOptions?.runId ?? ctx.MessageSidFull ?? ctx.MessageSid ?? undefined;
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey: transcriptSessionKey,
|
||||
agentId: sessionAgentId,
|
||||
text: diagnosticText,
|
||||
...(idempotencySource
|
||||
? { idempotencyKey: `${idempotencySource}:message-tool-only-failure-diagnostic` }
|
||||
: {}),
|
||||
updateMode: "inline",
|
||||
config: cfg,
|
||||
});
|
||||
if (!result.ok) {
|
||||
logVerbose(
|
||||
`dispatch-from-config: message-tool-only failure diagnostic mirror skipped: ${result.reason}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
for (const reply of replies) {
|
||||
throwIfDispatchOperationAborted();
|
||||
// Suppress reasoning payloads from channel delivery — channels using this
|
||||
@@ -2164,6 +2206,7 @@ export async function dispatchReplyFromConfig(
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
await mirrorSuppressedSourceReplyFailureToTranscript(reply);
|
||||
continue;
|
||||
}
|
||||
attemptedFinalDelivery = true;
|
||||
|
||||
@@ -2143,6 +2143,62 @@ describe("agent event handler", () => {
|
||||
expect(nodePayload.errorKind).toBe("rate_limit");
|
||||
});
|
||||
|
||||
it("surfaces message-tool-only non-deliverable ends as chat diagnostics", () => {
|
||||
const { broadcast, nodeSendToSession, clearAgentRunContext, handler } = createHarness({
|
||||
lifecycleErrorRetryGraceMs: 0,
|
||||
resolveSessionKeyForRun: () => "session-room-event-hidden",
|
||||
});
|
||||
registerAgentRunContext("run-room-event-hidden", {
|
||||
sessionKey: "session-room-event-hidden",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
suppressPromptPersistence: true,
|
||||
inboundEventKind: "room_event",
|
||||
});
|
||||
|
||||
handler({
|
||||
runId: "run-room-event-hidden",
|
||||
seq: 1,
|
||||
stream: "lifecycle",
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
phase: "end",
|
||||
stopReason: "toolUse",
|
||||
livenessState: "abandoned",
|
||||
replayInvalid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = requireRecord(
|
||||
chatBroadcastCalls(broadcast).at(-1)?.[1],
|
||||
"message-tool-only chat payload",
|
||||
);
|
||||
expect(payload.state).toBe("error");
|
||||
expect(payload.sessionKey).toBe("session-room-event-hidden");
|
||||
expect(payload.errorMessage).toContain(
|
||||
"Message-tool-only turn ended without a visible source reply.",
|
||||
);
|
||||
expect(payload.errorMessage).toContain("The inbound prompt was not saved to chat history");
|
||||
expect(payload.errorMessage).toContain("inbound=room_event");
|
||||
expect(payload.errorMessage).toContain("stopReason=toolUse");
|
||||
|
||||
const message = requireRecord(payload.message, "message-tool-only diagnostic message");
|
||||
expect(message.role).toBe("assistant");
|
||||
expect(message.stopReason).toBe("error");
|
||||
expect(message.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: payload.errorMessage,
|
||||
},
|
||||
]);
|
||||
|
||||
const nodePayload = requireRecord(
|
||||
sessionChatCalls(nodeSendToSession).at(-1)?.[2],
|
||||
"message-tool-only node payload",
|
||||
);
|
||||
expect(nodePayload.errorMessage).toBe(payload.errorMessage);
|
||||
expect(clearAgentRunContext).toHaveBeenCalledWith("run-room-event-hidden");
|
||||
});
|
||||
|
||||
it("suppresses delayed lifecycle chat errors for active chat.send runs while still cleaning up", () => {
|
||||
vi.useFakeTimers();
|
||||
const { broadcast, clearAgentRunContext, agentRunSeq, handler } = createHarness({
|
||||
|
||||
@@ -2,7 +2,11 @@ import { resolveToolSearchCodeDisplayTarget } from "../agents/tool-display-commo
|
||||
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "../auto-reply/heartbeat.js";
|
||||
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { getRuntimeConfig } from "../config/io.js";
|
||||
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
||||
import {
|
||||
type AgentEventPayload,
|
||||
type AgentRunContext,
|
||||
getAgentRunContext,
|
||||
} from "../infra/agent-events.js";
|
||||
import { detectErrorKind, type ErrorKind } from "../infra/errors.js";
|
||||
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
|
||||
import { isAcpSessionKey, isSubagentSessionKey } from "../sessions/session-key-utils.js";
|
||||
@@ -175,6 +179,79 @@ function readChatErrorKind(value: unknown): ErrorKind | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
type MessageToolOnlyLifecycleDiagnostic = {
|
||||
text: string;
|
||||
treatAsError: boolean;
|
||||
};
|
||||
|
||||
function buildDiagnosticChatMessage(text: string) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
timestamp: Date.now(),
|
||||
stopReason: "error",
|
||||
};
|
||||
}
|
||||
|
||||
function appendDiagnosticMessage(errorMessage: string | undefined, diagnostic: string): string {
|
||||
const trimmedError = errorMessage?.trim();
|
||||
if (!trimmedError || trimmedError === diagnostic) {
|
||||
return diagnostic;
|
||||
}
|
||||
return `${trimmedError}\n\n${diagnostic}`;
|
||||
}
|
||||
|
||||
function resolveMessageToolOnlyLifecycleDiagnostic(params: {
|
||||
runContext?: AgentRunContext;
|
||||
lifecyclePhase: "end" | "error";
|
||||
evt: AgentEventPayload;
|
||||
stopReason?: string;
|
||||
}): MessageToolOnlyLifecycleDiagnostic | undefined {
|
||||
if (params.runContext?.sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return undefined;
|
||||
}
|
||||
const livenessState = readString(params.evt.data?.livenessState);
|
||||
const inboundEventKind = readString(params.runContext.inboundEventKind);
|
||||
const replayInvalid = params.evt.data?.replayInvalid === true;
|
||||
const stopReason = params.stopReason ?? readString(params.evt.data?.stopReason);
|
||||
const nonDeliverableEnd =
|
||||
params.lifecyclePhase === "end" &&
|
||||
(livenessState === "abandoned" || (replayInvalid && stopReason === "toolUse"));
|
||||
if (params.lifecyclePhase !== "error" && !nonDeliverableEnd) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const promptSuppressed =
|
||||
params.runContext.suppressPromptPersistence === true || inboundEventKind === "room_event";
|
||||
const outcome =
|
||||
params.lifecyclePhase === "error"
|
||||
? "Message-tool-only turn failed."
|
||||
: "Message-tool-only turn ended without a visible source reply.";
|
||||
const promptVisibility = promptSuppressed
|
||||
? "The inbound prompt was not saved to chat history by design."
|
||||
: "Automatic source delivery was suppressed by policy.";
|
||||
const diagnostics = [
|
||||
inboundEventKind ? `inbound=${inboundEventKind}` : null,
|
||||
"mode=message_tool_only",
|
||||
stopReason ? `stopReason=${stopReason}` : null,
|
||||
livenessState ? `liveness=${livenessState}` : null,
|
||||
replayInvalid ? "replayInvalid=true" : null,
|
||||
].filter((value): value is string => Boolean(value));
|
||||
const diagnosticsText = diagnostics.length ? ` Diagnostics: ${diagnostics.join(", ")}.` : "";
|
||||
|
||||
return {
|
||||
treatAsError: true,
|
||||
text:
|
||||
`OpenClaw diagnostic: ${outcome} ${promptVisibility} ` +
|
||||
"A visible reply for this turn must be sent through the `message` tool in the originating client." +
|
||||
diagnosticsText,
|
||||
};
|
||||
}
|
||||
|
||||
type BroadcastDelta = { deltaText: string; replace?: true };
|
||||
|
||||
function resolveBroadcastDelta(params: {
|
||||
@@ -377,7 +454,8 @@ export function createAgentEventHandler({
|
||||
const chatLink = chatRunState.registry.peek(evt.runId);
|
||||
const eventSessionKey =
|
||||
typeof evt.sessionKey === "string" && evt.sessionKey.trim() ? evt.sessionKey : undefined;
|
||||
const isControlUiVisible = getAgentRunContext(evt.runId)?.isControlUiVisible ?? true;
|
||||
const runContext = getAgentRunContext(evt.runId);
|
||||
const isControlUiVisible = runContext?.isControlUiVisible ?? true;
|
||||
const sessionKey =
|
||||
chatLink?.sessionKey ?? eventSessionKey ?? resolveSessionKeyForRun(evt.runId);
|
||||
const clientRunId = chatLink?.clientRunId ?? evt.runId;
|
||||
@@ -391,6 +469,16 @@ export function createAgentEventHandler({
|
||||
typeof evt.data?.stopReason === "string" ? evt.data.stopReason : undefined;
|
||||
const evtErrorKind =
|
||||
readChatErrorKind(evt.data?.errorKind) ?? detectErrorKind(evt.data?.error);
|
||||
const sourceReplyDiagnostic = resolveMessageToolOnlyLifecycleDiagnostic({
|
||||
runContext,
|
||||
lifecyclePhase,
|
||||
evt,
|
||||
stopReason: evtStopReason,
|
||||
});
|
||||
const chatJobState =
|
||||
lifecyclePhase === "error" || sourceReplyDiagnostic?.treatAsError === true
|
||||
? "error"
|
||||
: "done";
|
||||
if (chatLink) {
|
||||
const finished = chatRunState.registry.shift(evt.runId);
|
||||
if (!finished) {
|
||||
@@ -403,10 +491,11 @@ export function createAgentEventHandler({
|
||||
finished.clientRunId,
|
||||
evt.runId,
|
||||
evt.seq,
|
||||
lifecyclePhase === "error" ? "error" : "done",
|
||||
chatJobState,
|
||||
evt.data?.error,
|
||||
evtStopReason,
|
||||
evtErrorKind,
|
||||
sourceReplyDiagnostic?.text,
|
||||
);
|
||||
}
|
||||
} else if (!(opts?.skipChatErrorFinal && lifecyclePhase === "error")) {
|
||||
@@ -415,10 +504,11 @@ export function createAgentEventHandler({
|
||||
eventRunId,
|
||||
evt.runId,
|
||||
evt.seq,
|
||||
lifecyclePhase === "error" ? "error" : "done",
|
||||
chatJobState,
|
||||
evt.data?.error,
|
||||
evtStopReason,
|
||||
evtErrorKind,
|
||||
sourceReplyDiagnostic?.text,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -609,6 +699,7 @@ export function createAgentEventHandler({
|
||||
error?: unknown,
|
||||
stopReason?: string,
|
||||
errorKind?: ErrorKind,
|
||||
diagnosticMessage?: string,
|
||||
) => {
|
||||
const { text, shouldSuppressSilent } = resolveBufferedChatTextState(clientRunId, sourceRunId, {
|
||||
suppressLeadFragments: false,
|
||||
@@ -626,6 +717,8 @@ export function createAgentEventHandler({
|
||||
clearAgentTextThrottleState(clientRunId);
|
||||
const spawnedBy = resolveSpawnedBy(sessionKey);
|
||||
if (jobState === "done") {
|
||||
const finalText =
|
||||
diagnosticMessage && text ? `${text}\n\n${diagnosticMessage}` : diagnosticMessage || text;
|
||||
const payload = {
|
||||
runId: clientRunId,
|
||||
sessionKey,
|
||||
@@ -634,11 +727,12 @@ export function createAgentEventHandler({
|
||||
state: "final" as const,
|
||||
...(stopReason && { stopReason }),
|
||||
message:
|
||||
text && !shouldSuppressSilent
|
||||
finalText && !shouldSuppressSilent
|
||||
? {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
content: [{ type: "text", text: finalText }],
|
||||
timestamp: Date.now(),
|
||||
...(diagnosticMessage ? { stopReason: "error" } : {}),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
@@ -646,14 +740,19 @@ export function createAgentEventHandler({
|
||||
nodeSendToSession(sessionKey, "chat", payload);
|
||||
return;
|
||||
}
|
||||
const formattedError = error ? formatForLog(error) : undefined;
|
||||
const errorMessage = diagnosticMessage
|
||||
? appendDiagnosticMessage(formattedError, diagnosticMessage)
|
||||
: formattedError;
|
||||
const payload = {
|
||||
runId: clientRunId,
|
||||
sessionKey,
|
||||
...(spawnedBy && { spawnedBy }),
|
||||
seq,
|
||||
state: "error" as const,
|
||||
errorMessage: error ? formatForLog(error) : undefined,
|
||||
errorMessage,
|
||||
...(errorKind && { errorKind }),
|
||||
...(diagnosticMessage ? { message: buildDiagnosticChatMessage(diagnosticMessage) } : {}),
|
||||
};
|
||||
broadcast("chat", payload);
|
||||
nodeSendToSession(sessionKey, "chat", payload);
|
||||
|
||||
@@ -149,6 +149,9 @@ describe("agent-events sequencing", () => {
|
||||
registerAgentRunContext("run-ctx", {
|
||||
verboseLevel: "full",
|
||||
isHeartbeat: true,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
suppressPromptPersistence: true,
|
||||
inboundEventKind: "room_event",
|
||||
lastActiveAt: 12_345,
|
||||
});
|
||||
|
||||
@@ -157,6 +160,9 @@ describe("agent-events sequencing", () => {
|
||||
expect(context?.verboseLevel).toBe("full");
|
||||
expect(context?.isHeartbeat).toBe(true);
|
||||
expect(context?.isControlUiVisible).toBe(true);
|
||||
expect(context?.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(context?.suppressPromptPersistence).toBe(true);
|
||||
expect(context?.inboundEventKind).toBe("room_event");
|
||||
expect(context?.lastActiveAt).toBe(12_345);
|
||||
});
|
||||
|
||||
|
||||
@@ -112,6 +112,12 @@ export type AgentRunContext = {
|
||||
sessionKey?: string;
|
||||
verboseLevel?: VerboseLevel;
|
||||
isHeartbeat?: boolean;
|
||||
/** Source reply visibility contract active for this run. */
|
||||
sourceReplyDeliveryMode?: "automatic" | "message_tool_only";
|
||||
/** The current user prompt is runtime context only and should not appear in chat.history. */
|
||||
suppressPromptPersistence?: boolean;
|
||||
/** Channel inbound turn classification, when known. */
|
||||
inboundEventKind?: "user_request" | "room_event" | (string & {});
|
||||
/** Whether control UI clients should receive chat/agent updates for this run. */
|
||||
isControlUiVisible?: boolean;
|
||||
/** Timestamp when this context was first registered (for TTL-based cleanup). */
|
||||
@@ -161,6 +167,15 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
|
||||
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
||||
existing.isHeartbeat = context.isHeartbeat;
|
||||
}
|
||||
if (context.sourceReplyDeliveryMode !== undefined) {
|
||||
existing.sourceReplyDeliveryMode = context.sourceReplyDeliveryMode;
|
||||
}
|
||||
if (context.suppressPromptPersistence !== undefined) {
|
||||
existing.suppressPromptPersistence = context.suppressPromptPersistence;
|
||||
}
|
||||
if (context.inboundEventKind !== undefined) {
|
||||
existing.inboundEventKind = context.inboundEventKind;
|
||||
}
|
||||
if (context.registeredAt !== undefined) {
|
||||
existing.registeredAt = context.registeredAt;
|
||||
}
|
||||
|
||||
@@ -456,6 +456,40 @@ describe("handleChatEvent", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("appends diagnostic messages carried by chat error events", () => {
|
||||
const state = createState({
|
||||
sessionKey: "main",
|
||||
chatRunId: "run-1",
|
||||
chatStream: "",
|
||||
chatStreamStartedAt: 100,
|
||||
});
|
||||
const diagnosticMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "OpenClaw diagnostic: message-tool-only turn failed.",
|
||||
},
|
||||
],
|
||||
stopReason: "error",
|
||||
};
|
||||
|
||||
expect(
|
||||
handleChatEvent(state, {
|
||||
runId: "run-1",
|
||||
sessionKey: "main",
|
||||
state: "error",
|
||||
errorMessage: "message-tool-only turn failed",
|
||||
message: diagnosticMessage,
|
||||
}),
|
||||
).toBe("error");
|
||||
|
||||
expect(state.chatMessages).toEqual([diagnosticMessage]);
|
||||
expect(state.lastError).toBe("message-tool-only turn failed");
|
||||
expect(state.chatRunId).toBeNull();
|
||||
expect(state.chatStream).toBeNull();
|
||||
});
|
||||
|
||||
it("persists streamed text when final event carries no message", () => {
|
||||
const existingMessage = {
|
||||
role: "user",
|
||||
|
||||
@@ -752,6 +752,10 @@ export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
|
||||
}
|
||||
reconcileTerminalRun("interrupted", "killed");
|
||||
} else if (payload.state === "error") {
|
||||
const diagnosticMessage = normalizeFinalAssistantMessage(payload.message);
|
||||
if (diagnosticMessage && !shouldHideAssistantChatMessage(diagnosticMessage)) {
|
||||
state.chatMessages = [...state.chatMessages, diagnosticMessage];
|
||||
}
|
||||
reconcileTerminalRun("interrupted", "failed");
|
||||
state.lastError = payload.errorMessage ?? "chat error";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user