Skip to content

Commit 85a7c29

Browse files
Merge remote-tracking branch 'origin/main' into pe/claw-179-skill-card-verify
# Conflicts: # extensions/canvas/src/cli.ts
2 parents 6545e88 + 2f710f5 commit 85a7c29

15 files changed

Lines changed: 494 additions & 34 deletions

docs/.i18n/glossary.zh-CN.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
"source": "OpenClaw",
44
"target": "OpenClaw"
55
},
6+
{
7+
"source": "macOS platform notes",
8+
"target": "macOS 平台说明"
9+
},
10+
{
11+
"source": "Logging",
12+
"target": "日志"
13+
},
614
{
715
"source": "iMessage",
816
"target": "iMessage"

docs/gateway/troubleshooting.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,58 @@ Related:
454454
- [Configuration](/gateway/configuration)
455455
- [Doctor](/gateway/doctor)
456456

457+
## macOS gateway silently stops responding, then resumes when you touch the dashboard
458+
459+
Use this when channels (Telegram, WhatsApp, etc.) on a macOS host go quiet for minutes to hours at a time, and the gateway appears to come back the moment you open the Control UI, SSH in, or otherwise interact with the host. There is usually no obvious symptom in `openclaw status` because by the time you look the gateway is alive again.
460+
461+
```bash
462+
ls ~/.openclaw/logs/stability/ | tail -5
463+
openclaw gateway stability --bundle latest
464+
pmset -g log | grep -iE "sleep|wake|maintenance" | tail -50
465+
launchctl print gui/$UID/ai.openclaw.gateway | grep -E "state|last exit|runs"
466+
```
467+
468+
Look for:
469+
470+
- One or more `*-uncaught_exception.json` bundles in `~/.openclaw/logs/stability/` with `error.code` set to a transient network code such as `ENETDOWN`, `ENETUNREACH`, `EHOSTUNREACH`, or `ECONNREFUSED`.
471+
- `pmset -g log` lines like `Entering Sleep state due to 'Maintenance Sleep'` or `en0 driver is slow (msg: WillChangeState to 0)` aligned with the crash timestamps. Power Nap / Maintenance Sleep briefly puts the Wi-Fi driver into state 0; any outbound `connect()` that lands in that window can fail with `ENETDOWN` even on a host that otherwise has full network connectivity.
472+
- `launchctl print` output showing `state = not running` with multiple recent `runs` and an exit code, especially when the gap between crash and the next launch is on the order of an hour rather than seconds. macOS launchd applies an undocumented respawn-protection gate after a crash burst that can stop honoring `KeepAlive=true` until an external trigger such as interactive login, dashboard connection, or `launchctl kickstart` re-arms it.
473+
474+
Common signatures:
475+
476+
- A stability bundle whose `error.code` is `ENETDOWN` or a sibling code, with the call stack pointing into Node `net` `lookupAndConnect` / `Socket.connect`. OpenClaw `2026.5.26` and newer classify these as benign transient network errors so they no longer propagate to the top-level uncaught handler; if you are on an older release, upgrade first.
477+
- Long quiet periods that end the instant you connect to the Control UI or SSH into the host: the user-visible activity is what re-arms launchd's respawn gate, not anything the dashboard does to the gateway.
478+
- `runs` count incrementing across the day with no corresponding `received SIG*; shutting down` line in `~/Library/Logs/openclaw/gateway.log`: clean shutdowns log a signal; transient crashes do not.
479+
480+
What to do:
481+
482+
1. **Upgrade the gateway** if you are running a release before `2026.5.26`. After upgrading, future `ENETDOWN` errors are logged as warnings instead of terminating the process.
483+
2. **Reduce maintenance sleep activity** on Mac mini / desktop hosts that are meant to run as always-on servers:
484+
485+
```bash
486+
sudo pmset -a sleep 0 disksleep 0 standby 0 powernap 0
487+
```
488+
489+
This significantly reduces, but does not entirely eliminate, the underlying driver flap. The system can still perform some maintenance sleeps for TCP keepalive and mDNS upkeep regardless of these flags.
490+
491+
3. **Add a liveness watchdog** so a future crash burst that gets parked by launchd is caught quickly:
492+
493+
```bash
494+
# Example launchd-aware liveness check, suitable for a 5-minute cron or LaunchAgent
495+
state=$(launchctl print gui/$UID/ai.openclaw.gateway 2>/dev/null | awk -F'= ' '/state =/ {print $2; exit}')
496+
if [ "$state" != "running" ]; then
497+
launchctl kickstart -k gui/$UID/ai.openclaw.gateway
498+
fi
499+
```
500+
501+
The point is to externally re-arm the respawn gate; `KeepAlive=true` alone is not sufficient on macOS after a crash burst.
502+
503+
Related:
504+
505+
- [macOS platform notes](/platforms/macos)
506+
- [Logging](/logging)
507+
- [Doctor](/gateway/doctor)
508+
457509
## Gateway exits during high memory use
458510

459511
Use this when the Gateway disappears under load, the supervisor reports an OOM-style restart, or logs mention `critical memory pressure bundle written`.

docs/platforms/macos.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Replace the label with `ai.openclaw.<profile>` when running a named profile.
4747
If the LaunchAgent isn't installed, enable it from the app or run
4848
`openclaw gateway install`.
4949

50+
If the gateway repeatedly disappears for minutes to hours and only resumes when you touch the Control UI or SSH into the host, see the troubleshooting note for macOS Maintenance Sleep / `ENETDOWN` crashes and launchd's respawn-protection gate in [Gateway troubleshooting](/gateway/troubleshooting#macos-gateway-silently-stops-responding-then-resumes-when-you-touch-the-dashboard).
51+
5052
## Node capabilities (mac)
5153

5254
The macOS app presents itself as a node. Common commands:

extensions/canvas/src/cli.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import {
88
resolveNodeFromNodeList,
99
type NodeMatchCandidate,
1010
} from "openclaw/plugin-sdk/gateway-runtime";
11-
import {
12-
parseStrictFiniteNumber,
13-
parseStrictPositiveInteger,
14-
} from "openclaw/plugin-sdk/number-runtime";
1511
import { defaultRuntime } from "openclaw/plugin-sdk/runtime";
1612
import {
1713
normalizeLowercaseStringOrEmpty,
@@ -74,6 +70,41 @@ export type CanvasCliDependencies = {
7470
type CanvasNodeCandidate = NodeMatchCandidate;
7571
type CanvasSnapshotRequestFormat = "png" | "jpeg";
7672

73+
function normalizeNumericString(value: string): string | undefined {
74+
const trimmed = value.trim();
75+
return trimmed ? trimmed : undefined;
76+
}
77+
78+
function parseStrictPositiveInteger(value: unknown): number | undefined {
79+
if (typeof value === "number") {
80+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
81+
}
82+
if (typeof value !== "string") {
83+
return undefined;
84+
}
85+
const normalized = normalizeNumericString(value);
86+
if (!normalized || !/^\+?\d+$/.test(normalized)) {
87+
return undefined;
88+
}
89+
const parsed = Number(normalized);
90+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
91+
}
92+
93+
function parseStrictFiniteNumber(value: unknown): number | undefined {
94+
if (typeof value === "number") {
95+
return Number.isFinite(value) ? value : undefined;
96+
}
97+
if (typeof value !== "string") {
98+
return undefined;
99+
}
100+
const normalized = normalizeNumericString(value);
101+
if (!normalized || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(normalized)) {
102+
return undefined;
103+
}
104+
const parsed = Number(normalized);
105+
return Number.isFinite(parsed) ? parsed : undefined;
106+
}
107+
77108
function parseCanvasSnapshotRequestFormat(raw: unknown): CanvasSnapshotRequestFormat {
78109
const format = normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw) ?? "jpg");
79110
switch (format) {

extensions/codex/src/app-server/attempt-startup.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ export async function startCodexAttemptThread(params: {
8484
effectiveWorkspace: string;
8585
dynamicTools: CodexDynamicToolSpec[];
8686
developerInstructions: string | undefined;
87-
finalConfigPatch: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
87+
finalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
88+
buildFinalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["buildFinalConfigPatch"];
89+
nativeHookRelayGeneration?: string;
8890
bundleMcpThreadConfig: CodexBundleMcpThreadConfig;
8991
nativeToolSurfaceEnabled: boolean;
9092
sandboxExecServerEnabled: boolean;
@@ -253,6 +255,8 @@ export async function startCodexAttemptThread(params: {
253255
developerInstructions: params.developerInstructions,
254256
config: threadConfig,
255257
finalConfigPatch: params.finalConfigPatch,
258+
buildFinalConfigPatch: params.buildFinalConfigPatch,
259+
nativeHookRelayGeneration: params.nativeHookRelayGeneration,
256260
nativeCodeModeEnabled: params.nativeToolSurfaceEnabled,
257261
nativeCodeModeOnlyEnabled: params.appServer.codeModeOnly,
258262
userMcpServersEnabled: params.nativeToolSurfaceEnabled,

extensions/codex/src/app-server/native-hook-relay.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export function createCodexNativeHookRelay(params: {
9595
gatewayTimeoutMs?: number;
9696
}
9797
| undefined;
98+
generation?: string;
99+
generationMismatchGraceMs?: number;
98100
events: readonly NativeHookRelayEvent[];
99101
agentId: string | undefined;
100102
sessionId: string;
@@ -117,6 +119,10 @@ export function createCodexNativeHookRelay(params: {
117119
sessionId: params.sessionId,
118120
sessionKey: params.sessionKey,
119121
}),
122+
...(params.generation ? { generation: params.generation } : {}),
123+
...(params.generationMismatchGraceMs
124+
? { generationMismatchGraceMs: params.generationMismatchGraceMs }
125+
: {}),
120126
...(params.agentId ? { agentId: params.agentId } : {}),
121127
sessionId: params.sessionId,
122128
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),

extensions/codex/src/app-server/run-attempt-test-harness.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,24 @@ export function createResumeHarness() {
394394
}
395395

396396
export function extractRelayIdFromThreadRequest(params: unknown): string {
397+
const command = extractNativeHookRelayCommandFromThreadRequest(params);
398+
const match = command.match(/--relay-id ([^ ]+)/);
399+
if (!match?.[1]) {
400+
throw new Error(`relay id missing from command: ${command}`);
401+
}
402+
return match[1];
403+
}
404+
405+
export function extractGenerationFromThreadRequest(params: unknown): string {
406+
const command = extractNativeHookRelayCommandFromThreadRequest(params);
407+
const match = command.match(/--generation ([^ ]+)/);
408+
if (!match?.[1]) {
409+
throw new Error(`relay generation missing from command: ${command}`);
410+
}
411+
return match[1];
412+
}
413+
414+
function extractNativeHookRelayCommandFromThreadRequest(params: unknown): string {
397415
const config = (params as { config?: Record<string, unknown> }).config;
398416
let command: string | undefined;
399417
for (const key of [
@@ -416,11 +434,10 @@ export function extractRelayIdFromThreadRequest(params: unknown): string {
416434
break;
417435
}
418436
}
419-
const match = command?.match(/--relay-id ([^ ]+)/);
420-
if (!match?.[1]) {
421-
throw new Error(`relay id missing from command: ${command}`);
437+
if (!command) {
438+
throw new Error("native hook relay command missing from thread request");
422439
}
423-
return match[1];
440+
return command;
424441
}
425442

426443
type RuntimeDynamicToolForTest = Parameters<

0 commit comments

Comments
 (0)