fix: restore recent native, browser, and audio behavior (#107631)

* fix: repair recent landed regressions

* chore: keep release changelog owned

* test(macos): align CLI fixtures with Node floor

* fix(macos): document Node runtime floor contract

* style(auto-reply): restore runtime mode formatting
This commit is contained in:
Peter Steinberger
2026-07-14 10:47:33 -07:00
committed by GitHub
parent e297bc2ac5
commit 96eaf2be80
12 changed files with 218 additions and 46 deletions
+33 -1
View File
@@ -4767,6 +4767,7 @@ def mask_reference_declaration_evidence(text: str) -> str:
def bare_code_reference(
text: str,
start: int,
end: int,
separator: str,
value: str,
) -> bool:
@@ -4796,6 +4797,31 @@ def bare_code_reference(
re.DOTALL,
):
return True
# TS/JS named-function parameter annotations use PascalCase type names.
function_prefix = re.search(
r"\bfunction\b[^()\r\n]*\((?P<parameters>[^()]*)$",
declaration,
)
annotation_suffix = re.match(
r"[ \t\r\n]*(?P<terminator>[,)=])",
text[end:],
)
if (
function_prefix is not None
and re.fullmatch(
r"\s*(?:\.\.\.\s*)?",
split_top_level_call_arguments(
function_prefix.group("parameters")
)[-1],
)
and annotation_suffix is not None
):
if annotation_suffix.group("terminator") != "=":
return True
return not fallback_secret_risk(
text[end + annotation_suffix.end() :],
minimum_length=8,
)
if camel_reference is None and snake_reference is None:
return False
return bool(
@@ -4909,7 +4935,13 @@ def secret_text_risk(text: str) -> bool:
return True
if (
match.group("bare_value") is not None
and bare_code_reference(text, match.start(), separator, value)
and bare_code_reference(
text,
match.start(),
match.end(),
separator,
value,
)
and safe_secret_assignment_suffix(text, match.end())
):
continue
@@ -1762,6 +1762,44 @@ class AutoreviewHardeningTests(unittest.TestCase):
)
)
def test_secret_detector_allows_typescript_function_parameter_types(self) -> None:
signature = (
"function formatCredentialLabel("
+ "credential"
+ ": ClaudeCliReadableCredential"
+ "): string {"
)
access_key = "AKIA" + "ABCDEFGHIJKLMNOP"
secret_assignment = "const api" + 'Key = "' + access_key + '";'
literal_value = "actual-production-" + "secret"
parameter_name = "api" + "Key"
type_name = "Api" + "Credential"
typed_default = (
"function connect("
+ parameter_name
+ ": "
+ type_name
+ ' = "'
+ literal_value
+ '") {}'
)
benign_default = (
"function connect("
+ parameter_name
+ ": "
+ type_name
+ " = defaultCredential) {}"
)
self.assertFalse(self.helper["secret_text_risk"](signature))
self.assertFalse(self.helper["secret_text_risk"](benign_default))
self.assertTrue(
self.helper["secret_text_risk"](
signature + "\n " + secret_assignment + "\n}"
)
)
self.assertTrue(self.helper["secret_text_risk"](typed_default))
def test_secret_detector_handles_punctuation_and_multiline_diff_values(self) -> None:
value = "Correct-Horse!" + "@Battery$Staple"
patch = (
@@ -183,7 +183,7 @@ struct WatchChatStatusLocalizationTests {
],
"gatewayConnected": false,
"agentName": "Main",
"agentAvatarURL": "https://example.com/avatar.png",
"agentAvatarUrl": "https://example.com/avatar.png",
"sessionKey": "main",
"talkStatus": [
"code": OpenClawWatchAppStatusCode.talkOff.rawValue,
@@ -6904,6 +6904,7 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
gatewayStatusText: "Connected",
gatewayConnected: true,
agentName: "Main",
agentAvatarURL: "https://example.com/avatar.png",
sessionKey: "main",
gatewayStableID: "gateway-a",
talkStatus: OpenClawWatchAppStatus(code: .talkOff),
@@ -6941,6 +6942,8 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
let nestedApprovals = try #require(
combined[OpenClawWatchPayloadType.execApprovalSnapshot.rawValue] as? [String: Any])
#expect(nestedApp["gatewayStableID"] as? String == "gateway-a")
#expect(nestedApp["agentAvatarUrl"] as? String == "https://example.com/avatar.png")
#expect(nestedApp["agentAvatarURL"] == nil)
let nestedChatStatus = try #require(nestedApp["chatStatus"] as? [String: Any])
#expect(nestedChatStatus["code"] as? String == "chatConnectIPhone")
#expect(nestedApp["chatStatusCode"] == nil)
@@ -541,7 +541,7 @@ struct WatchAppSnapshotMessage: Codable, Equatable {
let gatewayConnected = Self.boolValue(payload["gatewayConnected"])
let agentName = (payload["agentName"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let agentAvatarURL = (payload["agentAvatarURL"] as? String)?
let agentAvatarURL = (payload["agentAvatarUrl"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let agentAvatarText = (payload["agentAvatarText"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -53,18 +53,24 @@ enum RuntimeResolutionError: Error {
enum RuntimeLocator {
private static let logger = Logger(subsystem: "ai.openclaw", category: "runtime")
private static let minNode22 = RuntimeVersion(major: 22, minor: 19, patch: 0)
private static let minNode23 = RuntimeVersion(major: 23, minor: 11, patch: 0)
private static let supportedNodeRange = ">=22.19.0 <23 or >=23.11.0"
// Keep these floors aligned with package.json engines so the app never launches
// the gateway on an unsupported odd release or an older even-major runtime.
private static let minNode22 = RuntimeVersion(major: 22, minor: 22, patch: 3)
private static let minNode24 = RuntimeVersion(major: 24, minor: 15, patch: 0)
private static let minNode25 = RuntimeVersion(major: 25, minor: 9, patch: 0)
private static let supportedNodeRange = ">=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"
static func isSupportedNodeVersion(_ version: RuntimeVersion) -> Bool {
if version.major == self.minNode22.major {
return version >= self.minNode22
}
if version.major == self.minNode23.major {
return version >= self.minNode23
if version.major == self.minNode24.major {
return version >= self.minNode24
}
return version.major > self.minNode23.major
if version.major == self.minNode25.major {
return version >= self.minNode25
}
return version.major > self.minNode25.major
}
static func resolve(
@@ -67,7 +67,7 @@ import Testing
let nodePath = tmp.appendingPathComponent("node_modules/.bin/node")
let scriptPath = tmp.appendingPathComponent("bin/openclaw.js")
try makeExecutableForTests(at: nodePath)
try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
try makeExecutableForTests(at: scriptPath)
@@ -162,7 +162,7 @@ import Testing
let binDir = tmp.appendingPathComponent("bin")
let nodePath = binDir.appendingPathComponent("node")
try makeExecutableForTests(at: nodePath)
try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
let cmd = CommandResolver.openclawCommand(
@@ -215,7 +215,7 @@ import Testing
@Test func `node manager runtimes precede system runtimes`() throws {
let home = try makeTempDirForTests()
let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.19.0/bin")
let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.22.3/bin")
try makeExecutableForTests(at: nodeManagerBin.appendingPathComponent("node"))
let paths = CommandResolver.preferredPaths(
@@ -16,7 +16,7 @@ struct RuntimeLocatorTests {
@Test func `resolve succeeds with valid node`() throws {
let script = """
#!/bin/sh
echo v22.19.0
echo v22.22.3
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
@@ -25,13 +25,13 @@ struct RuntimeLocatorTests {
return
}
#expect(res.path == node.path)
#expect(res.version == RuntimeVersion(major: 22, minor: 19, patch: 0))
#expect(res.version == RuntimeVersion(major: 22, minor: 22, patch: 3))
}
@Test func `resolve fails on boundary below minimum`() throws {
let script = """
#!/bin/sh
echo v22.18.9
echo v22.22.2
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
@@ -39,38 +39,38 @@ struct RuntimeLocatorTests {
Issue.record("Expected unsupported error, got \(result)")
return
}
#expect(found == RuntimeVersion(major: 22, minor: 18, patch: 9))
#expect(found == RuntimeVersion(major: 22, minor: 22, patch: 2))
#expect(path == node.path)
}
@Test func `resolve rejects early node 23`() throws {
let script = """
#!/bin/sh
echo v23.7.0
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
guard case let .failure(.unsupported(_, found, path, _)) = result else {
Issue.record("Expected unsupported error, got \(result)")
return
}
#expect(found == RuntimeVersion(major: 23, minor: 7, patch: 0))
#expect(path == node.path)
}
@Test func `resolve accepts node 23 with statement columns`() throws {
@Test func `resolve rejects node 23`() throws {
let script = """
#!/bin/sh
echo v23.11.0
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
guard case let .success(res) = result else {
Issue.record("Expected success, got \(result)")
guard case let .failure(.unsupported(_, found, path, _)) = result else {
Issue.record("Expected unsupported error, got \(result)")
return
}
#expect(res.path == node.path)
#expect(res.version == RuntimeVersion(major: 23, minor: 11, patch: 0))
#expect(found == RuntimeVersion(major: 23, minor: 11, patch: 0))
#expect(path == node.path)
}
@Test(arguments: [
("22.22.2", false),
("22.22.3", true),
("23.11.0", false),
("24.14.1", false),
("24.15.0", true),
("25.8.1", false),
("25.9.0", true),
("26.0.0", true),
])
func `node support matches the core runtime contract`(version: String, supported: Bool) throws {
let parsed = try #require(RuntimeVersion.from(string: version))
#expect(RuntimeLocator.isSupportedNodeVersion(parsed) == supported)
}
@Test func `resolve fails when too old`() throws {
@@ -105,7 +105,7 @@ struct RuntimeLocatorTests {
@Test func `describe failure includes paths`() {
let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"]))
#expect(msg.contains("Node >=22.19.0 <23 or >=23.11.0"))
#expect(msg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"))
#expect(msg.contains("PATH searched: /tmp/a:/tmp/b"))
let parseMsg = RuntimeLocator.describeFailure(
@@ -114,7 +114,7 @@ struct RuntimeLocatorTests {
raw: "garbage",
path: "/usr/local/bin/node",
searchPaths: ["/usr/local/bin"]))
#expect(parseMsg.contains("Node >=22.19.0 <23 or >=23.11.0"))
#expect(parseMsg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"))
}
@Test func `runtime version parses with leading V and metadata`() {
@@ -243,7 +243,9 @@ export async function getCachedChromeGraphicsDiagnostics(
running.graphicsDiagnosticsPending ??= load();
try {
const diagnostics = await running.graphicsDiagnosticsPending;
running.graphicsDiagnostics = diagnostics;
if (diagnostics.status === "available") {
running.graphicsDiagnostics = diagnostics;
}
return diagnostics;
} finally {
running.graphicsDiagnosticsPending = undefined;
@@ -339,6 +339,60 @@ describe("basic browser routes", () => {
);
});
it("retries unavailable graphics diagnostics and caches the first available result", async () => {
const unavailable = {
status: "unavailable",
observedAt: 123,
reason: "SystemInfo.getInfo timed out",
} as const;
const available = {
status: "available",
observedAt: 456,
acceleration: "hardware",
renderer: "ANGLE (Intel)",
vendor: "Intel",
version: "OpenGL ES 3.0",
backend: "(gl=angle,angle=metal)",
devices: [],
featureStatus: { webgl: "enabled" },
disabledFeatures: [],
driverBugWorkarounds: [],
videoDecoding: [],
videoEncoding: [],
} as const;
inspectChromeGraphicsDiagnosticsMock
.mockResolvedValueOnce(unavailable)
.mockResolvedValue(available);
const state = createManagedProfileState(
{},
{
isHttpReachable: async () => true,
isTransportAvailable: async () => true,
},
);
const profile = (state.forProfile() as { profile: unknown }).profile as never;
state.profiles.set("openclaw", {
profile,
running: {
pid: 222,
exe: { kind: "chromium", path: "/usr/bin/chromium" },
userDataDir: "/tmp/openclaw-profile",
cdpPort: 18800,
startedAt: Date.now(),
proc: {} as never,
},
});
const first = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
const second = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
const third = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
expect(responseBodyRecord(first).graphics).toEqual(unavailable);
expect(responseBodyRecord(second).graphics).toEqual(available);
expect(responseBodyRecord(third).graphics).toEqual(available);
expect(inspectChromeGraphicsDiagnosticsMock).toHaveBeenCalledTimes(2);
});
it("does not inspect graphics while the managed process is pending reconcile", async () => {
const state = createManagedProfileState(
{},
@@ -167,6 +167,39 @@ describe("local audio selection", () => {
capableBackend: "metal",
observedBackend: "metal",
});
for (const failedBackend of ["Metal", "MTL0", "CUDA0"]) {
expect(
recordLocalAudioBackendObservation({
command: "whisper-cli",
args: ["-m", modelPath, "-otxt", "-of", "{{OutputBase}}", "-nt", "{{MediaPath}}"],
output: [
`whisper_backend_init_gpu: using ${failedBackend} backend`,
`whisper_backend_init_gpu: failed to initialize ${failedBackend} backend`,
].join("\n"),
}),
).toBe("cpu");
const failedAccelerationSelection = await inspectLocalAudioSelection({
env: {
WHISPER_CPP_MODEL: modelPath,
SHERPA_ONNX_MODEL_DIR: sherpaDir,
},
platform: "darwin",
arch: "arm64",
resolveBinary: async (name) =>
name === "whisper-cli"
? "/opt/homebrew/bin/whisper-cli"
: name === "sherpa-onnx-offline"
? "/usr/local/bin/sherpa-onnx-offline"
: null,
resolveRealpath: async () => "/opt/homebrew/Cellar/whisper-cpp/1.9.1/bin/whisper-cli",
inspectLinkedLibraries: async () => null,
});
expect(failedAccelerationSelection.selected).toMatchObject({ id: "sherpa-onnx-offline" });
expect(
failedAccelerationSelection.candidates.find((candidate) => candidate.id === "whisper-cli"),
).toMatchObject({ observedBackend: "cpu" });
}
});
it("reports Parakeet as MLX-capable without treating capability as observation", async () => {
+11 -7
View File
@@ -91,13 +91,17 @@ export function recordLocalAudioBackendObservation(params: {
if (commandId(params.command) !== "whisper-cli") {
return undefined;
}
const backend = /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output)
? "metal"
: /using\s+CUDA\d*\s+backend/i.test(params.output)
? "cuda"
: /using\s+CPU\s+backend|no GPU found/i.test(params.output)
? "cpu"
: undefined;
const acceleratorInitializationFailed =
/failed to initialize\s+(?:MTL\d+|Metal|CUDA\d*)\s+backend/i.test(params.output);
const backend = acceleratorInitializationFailed
? "cpu"
: /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output)
? "metal"
: /using\s+CUDA\d*\s+backend/i.test(params.output)
? "cuda"
: /using\s+CPU\s+backend|no GPU found/i.test(params.output)
? "cpu"
: undefined;
if (backend) {
observedBackendCache.set(observationKey(params), backend);
}