mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
Fix LaunchAgent missing TMPDIR causing SQLITE_CANTOPEN on macOS (#20512)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 25ba59765d
Co-authored-by: Clawborn <261310391+Clawborn@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn.
|
||||||
- iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman.
|
- iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman.
|
||||||
- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor.
|
- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor.
|
||||||
- Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky.
|
- Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky.
|
||||||
|
|||||||
@@ -151,6 +151,26 @@ describe("launchd install", () => {
|
|||||||
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
|
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(enableIndex).toBeLessThan(bootstrapIndex);
|
expect(enableIndex).toBeLessThan(bootstrapIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes TMPDIR to LaunchAgent environment when provided", async () => {
|
||||||
|
const env: Record<string, string | undefined> = {
|
||||||
|
HOME: "/Users/test",
|
||||||
|
OPENCLAW_PROFILE: "default",
|
||||||
|
};
|
||||||
|
const tmpDir = "/var/folders/xy/abc123/T/";
|
||||||
|
await installLaunchAgent({
|
||||||
|
env,
|
||||||
|
stdout: new PassThrough(),
|
||||||
|
programArguments: ["node", "-e", "process.exit(0)"],
|
||||||
|
environment: { TMPDIR: tmpDir },
|
||||||
|
});
|
||||||
|
|
||||||
|
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||||
|
const plist = state.files.get(plistPath) ?? "";
|
||||||
|
expect(plist).toContain("<key>EnvironmentVariables</key>");
|
||||||
|
expect(plist).toContain("<key>TMPDIR</key>");
|
||||||
|
expect(plist).toContain(`<string>${tmpDir}</string>`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveLaunchAgentPlistPath", () => {
|
describe("resolveLaunchAgentPlistPath", () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveGatewayStateDir } from "./paths.js";
|
import { resolveGatewayStateDir } from "./paths.js";
|
||||||
@@ -282,6 +283,22 @@ describe("buildServiceEnvironment", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards TMPDIR from the host environment", () => {
|
||||||
|
const env = buildServiceEnvironment({
|
||||||
|
env: { HOME: "/home/user", TMPDIR: "/var/folders/xw/abc123/T/" },
|
||||||
|
port: 18789,
|
||||||
|
});
|
||||||
|
expect(env.TMPDIR).toBe("/var/folders/xw/abc123/T/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to os.tmpdir when TMPDIR is not set", () => {
|
||||||
|
const env = buildServiceEnvironment({
|
||||||
|
env: { HOME: "/home/user" },
|
||||||
|
port: 18789,
|
||||||
|
});
|
||||||
|
expect(env.TMPDIR).toBe(os.tmpdir());
|
||||||
|
});
|
||||||
|
|
||||||
it("uses profile-specific unit and label", () => {
|
it("uses profile-specific unit and label", () => {
|
||||||
const env = buildServiceEnvironment({
|
const env = buildServiceEnvironment({
|
||||||
env: { HOME: "/home/user", OPENCLAW_PROFILE: "work" },
|
env: { HOME: "/home/user", OPENCLAW_PROFILE: "work" },
|
||||||
@@ -301,6 +318,20 @@ describe("buildNodeServiceEnvironment", () => {
|
|||||||
});
|
});
|
||||||
expect(env.HOME).toBe("/home/user");
|
expect(env.HOME).toBe("/home/user");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards TMPDIR for node services", () => {
|
||||||
|
const env = buildNodeServiceEnvironment({
|
||||||
|
env: { HOME: "/home/user", TMPDIR: "/tmp/custom" },
|
||||||
|
});
|
||||||
|
expect(env.TMPDIR).toBe("/tmp/custom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to os.tmpdir for node services when TMPDIR is not set", () => {
|
||||||
|
const env = buildNodeServiceEnvironment({
|
||||||
|
env: { HOME: "/home/user" },
|
||||||
|
});
|
||||||
|
expect(env.TMPDIR).toBe(os.tmpdir());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveGatewayStateDir", () => {
|
describe("resolveGatewayStateDir", () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import {
|
import {
|
||||||
@@ -212,8 +213,11 @@ export function buildServiceEnvironment(params: {
|
|||||||
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||||
const stateDir = env.OPENCLAW_STATE_DIR;
|
const stateDir = env.OPENCLAW_STATE_DIR;
|
||||||
const configPath = env.OPENCLAW_CONFIG_PATH;
|
const configPath = env.OPENCLAW_CONFIG_PATH;
|
||||||
|
// Keep a usable temp directory for supervised services even when the host env omits TMPDIR.
|
||||||
|
const tmpDir = env.TMPDIR?.trim() || os.tmpdir();
|
||||||
return {
|
return {
|
||||||
HOME: env.HOME,
|
HOME: env.HOME,
|
||||||
|
TMPDIR: tmpDir,
|
||||||
PATH: buildMinimalServicePath({ env }),
|
PATH: buildMinimalServicePath({ env }),
|
||||||
OPENCLAW_PROFILE: profile,
|
OPENCLAW_PROFILE: profile,
|
||||||
OPENCLAW_STATE_DIR: stateDir,
|
OPENCLAW_STATE_DIR: stateDir,
|
||||||
@@ -234,8 +238,10 @@ export function buildNodeServiceEnvironment(params: {
|
|||||||
const { env } = params;
|
const { env } = params;
|
||||||
const stateDir = env.OPENCLAW_STATE_DIR;
|
const stateDir = env.OPENCLAW_STATE_DIR;
|
||||||
const configPath = env.OPENCLAW_CONFIG_PATH;
|
const configPath = env.OPENCLAW_CONFIG_PATH;
|
||||||
|
const tmpDir = env.TMPDIR?.trim() || os.tmpdir();
|
||||||
return {
|
return {
|
||||||
HOME: env.HOME,
|
HOME: env.HOME,
|
||||||
|
TMPDIR: tmpDir,
|
||||||
PATH: buildMinimalServicePath({ env }),
|
PATH: buildMinimalServicePath({ env }),
|
||||||
OPENCLAW_STATE_DIR: stateDir,
|
OPENCLAW_STATE_DIR: stateDir,
|
||||||
OPENCLAW_CONFIG_PATH: configPath,
|
OPENCLAW_CONFIG_PATH: configPath,
|
||||||
|
|||||||
Reference in New Issue
Block a user