mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:57:39 +00:00
cron: infer payload kind for model-only update patches (openclaw#15664) thanks @rodrigouroz
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check (fails on current origin/main in src/memory/embedding-manager.test-harness.ts; unchanged by this PR) Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
|
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
|
||||||
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
|
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
|
||||||
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
|
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
|
||||||
|
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|
||||||
|
|||||||
71
src/agents/pi-embedded-subscribe.handlers.tools.test.ts
Normal file
71
src/agents/pi-embedded-subscribe.handlers.tools.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { handleToolExecutionStart } from "./pi-embedded-subscribe.handlers.tools.js";
|
||||||
|
|
||||||
|
function createTestContext() {
|
||||||
|
const onBlockReplyFlush = vi.fn();
|
||||||
|
const warn = vi.fn();
|
||||||
|
const ctx = {
|
||||||
|
params: {
|
||||||
|
runId: "run-test",
|
||||||
|
onBlockReplyFlush,
|
||||||
|
onAgentEvent: undefined,
|
||||||
|
onToolResult: undefined,
|
||||||
|
},
|
||||||
|
flushBlockReplyBuffer: vi.fn(),
|
||||||
|
hookRunner: undefined,
|
||||||
|
log: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
warn,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
toolMetaById: new Map<string, string | undefined>(),
|
||||||
|
toolSummaryById: new Set<string>(),
|
||||||
|
pendingMessagingTargets: new Map<string, unknown>(),
|
||||||
|
pendingMessagingTexts: new Map<string, string>(),
|
||||||
|
messagingToolSentTexts: [],
|
||||||
|
messagingToolSentTextsNormalized: [],
|
||||||
|
messagingToolSentTargets: [],
|
||||||
|
},
|
||||||
|
shouldEmitToolResult: () => false,
|
||||||
|
emitToolSummary: vi.fn(),
|
||||||
|
trimMessagingToolSent: vi.fn(),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return { ctx, warn, onBlockReplyFlush };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("handleToolExecutionStart read path checks", () => {
|
||||||
|
it("does not warn when read tool uses file_path alias", async () => {
|
||||||
|
const { ctx, warn, onBlockReplyFlush } = createTestContext();
|
||||||
|
|
||||||
|
await handleToolExecutionStart(
|
||||||
|
ctx as never,
|
||||||
|
{
|
||||||
|
type: "tool_execution_start",
|
||||||
|
toolName: "read",
|
||||||
|
toolCallId: "tool-1",
|
||||||
|
args: { file_path: "/tmp/example.txt" },
|
||||||
|
} as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onBlockReplyFlush).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns when read tool has neither path nor file_path", async () => {
|
||||||
|
const { ctx, warn } = createTestContext();
|
||||||
|
|
||||||
|
await handleToolExecutionStart(
|
||||||
|
ctx as never,
|
||||||
|
{
|
||||||
|
type: "tool_execution_start",
|
||||||
|
toolName: "read",
|
||||||
|
toolCallId: "tool-2",
|
||||||
|
args: {},
|
||||||
|
} as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(String(warn.mock.calls[0]?.[0] ?? "")).toContain("read tool called without path");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,12 +75,13 @@ export async function handleToolExecutionStart(
|
|||||||
|
|
||||||
if (toolName === "read") {
|
if (toolName === "read") {
|
||||||
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
||||||
const filePath =
|
const filePathValue =
|
||||||
typeof record.path === "string"
|
typeof record.path === "string"
|
||||||
? record.path.trim()
|
? record.path
|
||||||
: typeof record.file_path === "string"
|
: typeof record.file_path === "string"
|
||||||
? record.file_path.trim()
|
? record.file_path
|
||||||
: "";
|
: "";
|
||||||
|
const filePath = filePathValue.trim();
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
const argsPreview = typeof args === "string" ? args.slice(0, 200) : undefined;
|
const argsPreview = typeof args === "string" ? args.slice(0, 200) : undefined;
|
||||||
ctx.log.warn(
|
ctx.log.warn(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { normalizeCronJobCreate } from "./normalize.js";
|
import { normalizeCronJobCreate, normalizeCronJobPatch } from "./normalize.js";
|
||||||
|
|
||||||
describe("normalizeCronJobCreate", () => {
|
describe("normalizeCronJobCreate", () => {
|
||||||
it("maps legacy payload.provider to payload.channel and strips provider", () => {
|
it("maps legacy payload.provider to payload.channel and strips provider", () => {
|
||||||
@@ -293,3 +293,31 @@ describe("normalizeCronJobCreate", () => {
|
|||||||
expect(delivery.to).toBe("123");
|
expect(delivery.to).toBe("123");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("normalizeCronJobPatch", () => {
|
||||||
|
it("infers agentTurn kind for model-only payload patches", () => {
|
||||||
|
const normalized = normalizeCronJobPatch({
|
||||||
|
payload: {
|
||||||
|
model: "anthropic/claude-sonnet-4-5",
|
||||||
|
},
|
||||||
|
}) as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
|
const payload = normalized.payload as Record<string, unknown>;
|
||||||
|
expect(payload.kind).toBe("agentTurn");
|
||||||
|
expect(payload.model).toBe("anthropic/claude-sonnet-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not infer agentTurn kind for delivery-only legacy hints", () => {
|
||||||
|
const normalized = normalizeCronJobPatch({
|
||||||
|
payload: {
|
||||||
|
channel: "telegram",
|
||||||
|
to: "+15550001111",
|
||||||
|
},
|
||||||
|
}) as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
|
const payload = normalized.payload as Record<string, unknown>;
|
||||||
|
expect(payload.kind).toBeUndefined();
|
||||||
|
expect(payload.channel).toBe("telegram");
|
||||||
|
expect(payload.to).toBe("+15550001111");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -74,10 +74,18 @@ function coercePayload(payload: UnknownRecord) {
|
|||||||
if (!next.kind) {
|
if (!next.kind) {
|
||||||
const hasMessage = typeof next.message === "string" && next.message.trim().length > 0;
|
const hasMessage = typeof next.message === "string" && next.message.trim().length > 0;
|
||||||
const hasText = typeof next.text === "string" && next.text.trim().length > 0;
|
const hasText = typeof next.text === "string" && next.text.trim().length > 0;
|
||||||
|
const hasAgentTurnHint =
|
||||||
|
typeof next.model === "string" ||
|
||||||
|
typeof next.thinking === "string" ||
|
||||||
|
typeof next.timeoutSeconds === "number" ||
|
||||||
|
typeof next.allowUnsafeExternalContent === "boolean";
|
||||||
if (hasMessage) {
|
if (hasMessage) {
|
||||||
next.kind = "agentTurn";
|
next.kind = "agentTurn";
|
||||||
} else if (hasText) {
|
} else if (hasText) {
|
||||||
next.kind = "systemEvent";
|
next.kind = "systemEvent";
|
||||||
|
} else if (hasAgentTurnHint) {
|
||||||
|
// Accept partial agentTurn payload patches that only tweak agent-turn-only fields.
|
||||||
|
next.kind = "agentTurn";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof next.message === "string") {
|
if (typeof next.message === "string") {
|
||||||
|
|||||||
@@ -181,6 +181,28 @@ describe("gateway server cron", () => {
|
|||||||
expect(merged?.delivery?.channel).toBe("telegram");
|
expect(merged?.delivery?.channel).toBe("telegram");
|
||||||
expect(merged?.delivery?.to).toBe("19098680");
|
expect(merged?.delivery?.to).toBe("19098680");
|
||||||
|
|
||||||
|
const modelOnlyPatchRes = await rpcReq(ws, "cron.update", {
|
||||||
|
id: mergeJobId,
|
||||||
|
patch: {
|
||||||
|
payload: {
|
||||||
|
model: "anthropic/claude-sonnet-4-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(modelOnlyPatchRes.ok).toBe(true);
|
||||||
|
const modelOnlyPatched = modelOnlyPatchRes.payload as
|
||||||
|
| {
|
||||||
|
payload?: {
|
||||||
|
kind?: unknown;
|
||||||
|
message?: unknown;
|
||||||
|
model?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
expect(modelOnlyPatched?.payload?.kind).toBe("agentTurn");
|
||||||
|
expect(modelOnlyPatched?.payload?.message).toBe("hello");
|
||||||
|
expect(modelOnlyPatched?.payload?.model).toBe("anthropic/claude-sonnet-4-5");
|
||||||
|
|
||||||
const legacyDeliveryPatchRes = await rpcReq(ws, "cron.update", {
|
const legacyDeliveryPatchRes = await rpcReq(ws, "cron.update", {
|
||||||
id: mergeJobId,
|
id: mergeJobId,
|
||||||
patch: {
|
patch: {
|
||||||
|
|||||||
Reference in New Issue
Block a user