Skip to content

Commit 119fa5d

Browse files
committed
fix(cron): normalize node denial wrappers
1 parent a93da80 commit 119fa5d

4 files changed

Lines changed: 27 additions & 8 deletions

File tree

docs/automation/cron-jobs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
5151
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.
5252
- Isolated cron runs that receive the narrow cron self-cleanup grant can still read scheduler status, a self-filtered list of their current job, and that job's run history, so status/heartbeat checks can inspect their own schedule without gaining broader cron mutation access.
5353
- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
54-
- Isolated cron runs prefer structured execution-denial metadata from the embedded run, then fall back to known final summary/output markers such as `SYSTEM_RUN_DENIED` and `INVALID_REQUEST`, so a blocked command is not reported as a green run.
54+
- Isolated cron runs use structured execution-denial metadata from the embedded run, including node-host `UNAVAILABLE` wrappers whose nested error message starts with `SYSTEM_RUN_DENIED` or `INVALID_REQUEST`, so a blocked command is not reported as a green run while ordinary assistant prose is not treated as a denial.
5555
- Isolated cron runs also treat run-level agent failures as job errors even when no reply payload is produced, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful.
5656
- When an isolated agent-turn job reaches `timeoutSeconds`, cron aborts the underlying agent run and gives it a short cleanup window. If the run does not drain, Gateway-owned cleanup force-clears that run's session ownership before cron records the timeout, so queued chat work is not left behind a stale processing session.
5757
- If an isolated agent-turn stalls before the runner starts or before the first model call, cron records a phase-specific timeout such as `setup timed out before runner start` or `stalled before first model call (last phase: context-engine)`. These watchdogs cover embedded providers and CLI-backed providers before their external CLI process is actually started, and are capped independently from long `timeoutSeconds` values so cold-start/auth/context failures surface quickly instead of waiting for the full job budget.

src/agents/pi-embedded-subscribe.handlers.tools.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,8 @@ describe("handleToolExecutionEnd timeout metadata", () => {
580580
error: "UNAVAILABLE: SYSTEM_RUN_DENIED: approval required",
581581
gatewayCode: "UNAVAILABLE",
582582
nodeError: {
583-
code: "SYSTEM_RUN_DENIED",
584-
message: "approval required",
583+
code: "UNAVAILABLE",
584+
message: "SYSTEM_RUN_DENIED: approval required",
585585
},
586586
},
587587
},

src/agents/pi-embedded-subscribe.tools.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ describe("extractToolErrorMessage", () => {
6363
status: "failed",
6464
gatewayCode: "UNAVAILABLE",
6565
nodeError: {
66-
code: "SYSTEM_RUN_DENIED",
67-
message: "approval required",
66+
code: "UNAVAILABLE",
67+
message: "SYSTEM_RUN_DENIED: approval required",
6868
},
6969
},
7070
}),
@@ -107,8 +107,8 @@ describe("extractToolErrorMessage", () => {
107107
error.gatewayCode = "UNAVAILABLE";
108108
error.details = {
109109
nodeError: {
110-
code: "SYSTEM_RUN_DENIED",
111-
message: "approval required",
110+
code: "UNAVAILABLE",
111+
message: "SYSTEM_RUN_DENIED: approval required",
112112
},
113113
};
114114

src/agents/pi-embedded-subscribe.tools.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { normalizeToolName } from "./tool-policy.js";
1616

1717
const TOOL_RESULT_MAX_CHARS = 8000;
1818
const TOOL_ERROR_MAX_CHARS = 400;
19+
const TOOL_DENIAL_ERROR_CODES = ["SYSTEM_RUN_DENIED", "INVALID_REQUEST"] as const;
1920

2021
function truncateToolText(text: string): string {
2122
if (text.length <= TOOL_RESULT_MAX_CHARS) {
@@ -104,12 +105,30 @@ function readErrorCodeField(value: unknown): string | undefined {
104105
return typeof value === "string" ? normalizeOptionalString(value) : undefined;
105106
}
106107

108+
function readDenialErrorCodeFromMessage(value: unknown): string | undefined {
109+
const message = typeof value === "string" ? normalizeOptionalString(value) : undefined;
110+
if (!message) {
111+
return undefined;
112+
}
113+
for (const code of TOOL_DENIAL_ERROR_CODES) {
114+
if (message === code || message.startsWith(`${code}:`)) {
115+
return code;
116+
}
117+
}
118+
return undefined;
119+
}
120+
107121
function readNestedErrorCodeField(value: unknown): string | undefined {
108122
if (!value || typeof value !== "object") {
109123
return undefined;
110124
}
111125
const record = value as Record<string, unknown>;
112-
return readErrorCodeField(record.code) ?? readErrorCodeField(record.gatewayCode);
126+
return (
127+
readDenialErrorCodeFromMessage(record.message) ??
128+
readDenialErrorCodeFromMessage(record.error) ??
129+
readErrorCodeField(record.code) ??
130+
readErrorCodeField(record.gatewayCode)
131+
);
113132
}
114133

115134
function extractDirectErrorCodeField(value: unknown): string | undefined {

0 commit comments

Comments
 (0)