export function formatLinuxSdBackedStateDirWarning(
displayStateDir: string,
linuxSdBackedStateDir: LinuxSdBackedStateDir,
): string { const displayMountPoint =
linuxSdBackedStateDir.mountPoint === "/"
? "/"
: shortenHomePath(linuxSdBackedStateDir.mountPoint); const safeSource = escapeControlCharsForTerminal(linuxSdBackedStateDir.source); const safeFsType = escapeControlCharsForTerminal(linuxSdBackedStateDir.fsType); const safeMountPoint = escapeControlCharsForTerminal(displayMountPoint); return [
`- State directory appears to be on SD/eMMC storage (${displayStateDir}; device ${safeSource}, fs ${safeFsType}, mount ${safeMountPoint}).`, "- SD/eMMC media can be slower for random I/O and wear faster under session/log churn.", "- For better startup and state durability, prefer SSD/NVMe (or USB SSD on Raspberry Pi) for OPENCLAW_STATE_DIR.",
].join("\n");
}
// Cloud-sync roots should always be anchored to the OS account home on macOS. // OPENCLAW_HOME can relocate app data defaults, but iCloud/CloudStorage remain under the OS home. const homedir = deps?.homedir ?? os.homedir(); const roots = [
{
storage: "iCloud Drive" as const,
root: path.join(homedir, "Library", "Mobile Documents", "com~apple~CloudDocs"),
},
{
storage: "CloudStorage provider" as const,
root: path.join(homedir, "Library", "CloudStorage"),
},
]; const realPath = (deps?.resolveRealPath ?? tryResolveRealPath)(stateDir); // Prefer the resolved target path when available so symlink prefixes do not // misclassify local state dirs as cloud-synced. const candidates = realPath ? [path.resolve(realPath)] : [path.resolve(stateDir)];
for (const candidate of candidates) { for (const { storage, root } of roots) { if (isPathUnderRoot(candidate, root)) { return { path: candidate, storage };
}
}
}
returnnull;
}
function isPairingPolicy(value: unknown): boolean { return normalizeOptionalLowercaseString(value) === "pairing";
}
function hasPairingPolicy(value: unknown): boolean { const record = asNullableObjectRecord(value); if (!record) { returnfalse;
} if (isPairingPolicy(record.dmPolicy)) { returntrue;
} const dm = asNullableObjectRecord(record.dm); if (dm && isPairingPolicy(dm.policy)) { returntrue;
} const accounts = asNullableObjectRecord(record.accounts); if (!accounts) { returnfalse;
} for (const accountCfg of Object.values(accounts)) { if (hasPairingPolicy(accountCfg)) { returntrue;
}
} returnfalse;
}
function isSlashRoutingSessionKey(sessionKey: string): boolean { const raw = normalizeOptionalLowercaseString(sessionKey); if (!raw) { returnfalse;
} const scoped = parseAgentSessionKey(raw)?.rest ?? raw; return /^[^:]+:slash:[^:]+(?:$|:)/.test(scoped);
}
function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { if (env.OPENCLAW_OAUTH_DIR?.trim()) { returntrue;
} const channels = asNullableObjectRecord(cfg.channels); if (!channels) { returnfalse;
} const withPersistedAuth = new Set(
listConfiguredChannelIdsForReadOnlyScope({
config: cfg,
env,
cache: true,
}),
); const withoutPersistedAuth = new Set(
listConfiguredChannelIdsForReadOnlyScope({
config: cfg,
env,
cache: true,
includePersistedAuthState: false,
}),
); if ([...withPersistedAuth].some((channelId) => !withoutPersistedAuth.has(channelId))) { returntrue;
} // Pairing allowlists are persisted under credentials/<channel>-allowFrom.json. for (const [channelId, channelCfg] of Object.entries(channels)) { if (channelId === "defaults" || channelId === "modelByChannel") { continue;
} if (hasPairingPolicy(channelCfg)) { returntrue;
}
} returnfalse;
}
if (cloudSyncedStateDir) {
warnings.push(
[
`- State directory is under macOS cloud-synced storage (${displayStateDir}; ${cloudSyncedStateDir.storage}).`, "- This can cause slow I/O and sync/lock races for sessions and credentials.", "- Prefer a local non-synced state dir (for example: ~/.openclaw).",
` Set locally: OPENCLAW_STATE_DIR=~/.openclaw ${formatCliCommand("openclaw doctor")}`,
].join("\n"),
);
} if (linuxSdBackedStateDir) {
warnings.push(formatLinuxSdBackedStateDirWarning(displayStateDir, linuxSdBackedStateDir));
}
let stateDirExists = existsDir(stateDir); if (!stateDirExists) {
warnings.push(
`- CRITICAL: state directory missing (${displayStateDir}). Sessions, credentials, logs, and config are stored there.`,
); if (cfg.gateway?.mode === "remote") {
warnings.push( "- Gateway is in remote mode; run doctor on the remote host where the gateway runs.",
);
} const create = await prompter.confirmRuntimeRepair({
message: `Create ${displayStateDir} now?`,
initialValue: false,
}); if (create) { const created = ensureDir(stateDir); if (created.ok) {
changes.push(`- Created ${displayStateDir}`);
stateDirExists = true;
} else {
warnings.push(`- Failed to create ${displayStateDir}: ${created.error}`);
}
}
}
if (stateDirExists && !canWriteDir(stateDir)) {
warnings.push(`- State directory not writable (${displayStateDir}).`); const hint = dirPermissionHint(stateDir); if (hint) {
warnings.push(` ${hint}`);
} const repair = await prompter.confirmRuntimeRepair({
message: `Repair permissions on ${displayStateDir}?`,
initialValue: true,
}); if (repair) { try { const stat = fs.statSync(stateDir); const target = addUserRwx(stat.mode);
fs.chmodSync(stateDir, target);
changes.push(`- Repaired permissions on ${displayStateDir}`);
} catch (err) {
warnings.push(`- Failed to repair ${displayStateDir}: ${String(err)}`);
}
}
} if (stateDirExists && process.platform !== "win32") { try { const dirLstat = fs.lstatSync(stateDir); const isDirSymlink = dirLstat.isSymbolicLink(); // For symlinks, check the resolved target permissions instead of the // symlink itself (which always reports 777). Skip the warning only when // the target lives in a known immutable store (e.g. /nix/store/). const stat = isDirSymlink ? fs.statSync(stateDir) : dirLstat; const resolvedDir = isDirSymlink ? fs.realpathSync(stateDir) : stateDir; const isImmutableStore = resolvedDir.startsWith("/nix/store/"); if (!isImmutableStore && (stat.mode & 0o077) !== 0) {
warnings.push(
`- State directory permissions are too open (${displayStateDir}). Recommend chmod 700.`,
); const tighten = await prompter.confirmRuntimeRepair({
message: `Tighten permissions on ${displayStateDir} to 700?`,
initialValue: true,
}); if (tighten) {
fs.chmodSync(stateDir, 0o700);
changes.push(`- Tightened permissions on ${displayStateDir} to 700`);
}
}
} catch (err) {
warnings.push(`- Failed to read ${displayStateDir} permissions: ${String(err)}`);
}
}
if (configPath && existsFile(configPath) && process.platform !== "win32") { try { const configLstat = fs.lstatSync(configPath); const isSymlink = configLstat.isSymbolicLink(); // For symlinks, check the resolved target permissions. Skip the warning // only when the target lives in an immutable store (e.g. /nix/store/). const stat = isSymlink ? fs.statSync(configPath) : configLstat; const resolvedConfig = isSymlink ? fs.realpathSync(configPath) : configPath; const isImmutableConfig = resolvedConfig.startsWith("/nix/store/"); if (!isImmutableConfig && (stat.mode & 0o077) !== 0) {
warnings.push(
`- Config file is group/world readable (${displayConfigPath ?? configPath}). Recommend chmod 600.`,
); const tighten = await prompter.confirmRuntimeRepair({
message: `Tighten permissions on ${displayConfigPath ?? configPath} to 600?`,
initialValue: true,
}); if (tighten) {
fs.chmodSync(configPath, 0o600);
changes.push(`- Tightened permissions on ${displayConfigPath ?? configPath} to 600`);
}
}
} catch (err) {
warnings.push(
`- Failed to read config permissions (${displayConfigPath ?? configPath}): ${String(err)}`,
);
}
}
if (stateDirExists) { const dirCandidates = new Map<string, string>();
dirCandidates.set(sessionsDir, "Sessions dir");
dirCandidates.set(storeDir, "Session store dir"); if (requireOAuthDir) {
dirCandidates.set(oauthDir, "OAuth dir");
} elseif (!existsDir(oauthDir)) {
warnings.push(
`- OAuth dir not present (${displayOauthDir}). Skipping create because no WhatsApp/pairing channel config is active.`,
);
} const displayDirFor = (dir: string) => { if (dir === sessionsDir) { return displaySessionsDir;
} if (dir === storeDir) { return displayStoreDir;
} if (dir === oauthDir) { return displayOauthDir;
} return shortenHomePath(dir);
};
for (const [dir, label] of dirCandidates) { const displayDir = displayDirFor(dir); if (!existsDir(dir)) {
warnings.push(`- CRITICAL: ${label} missing (${displayDir}).`); const create = await prompter.confirmRuntimeRepair({
message: `Create ${label} at ${displayDir}?`,
initialValue: true,
}); if (create) { const created = ensureDir(dir); if (created.ok) {
changes.push(`- Created ${label}: ${displayDir}`);
} else {
warnings.push(`- Failed to create ${displayDir}: ${created.error}`);
}
} continue;
} if (!canWriteDir(dir)) {
warnings.push(`- ${label} not writable (${displayDir}).`); const hint = dirPermissionHint(dir); if (hint) {
warnings.push(` ${hint}`);
} const repair = await prompter.confirmRuntimeRepair({
message: `Repair permissions on ${label}?`,
initialValue: true,
}); if (repair) { try { const stat = fs.statSync(dir); const target = addUserRwx(stat.mode);
fs.chmodSync(dir, target);
changes.push(`- Repaired permissions on ${label}: ${displayDir}`);
} catch (err) {
warnings.push(`- Failed to repair ${displayDir}: ${String(err)}`);
}
}
}
}
}
const extraStateDirs = new Set<string>(); if (path.resolve(stateDir) !== path.resolve(defaultStateDir)) { if (existsDir(defaultStateDir)) {
extraStateDirs.add(defaultStateDir);
}
} for (const other of findOtherStateDirs(stateDir)) {
extraStateDirs.add(other);
} if (extraStateDirs.size > 0) {
warnings.push(
[ "- Multiple state directories detected. This can split session history.",
...Array.from(extraStateDirs).map((dir) => ` - ${shortenHomePath(dir)}`),
` Active state dir: ${displayStateDir}`,
].join("\n"),
);
}
const orphanAgentDirs = listOrphanAgentDirs(cfg, stateDir); if (orphanAgentDirs.length > 0) {
warnings.push(
[
`- Found ${countLabel(orphanAgentDirs.length, "agent directory", "agent directories")} on disk without a matching agents.list entry.`, " These agents can still have sessions/auth state on disk, but config-driven routing, identity, and model selection will ignore them.",
` Examples: ${formatOrphanAgentDirPreview(orphanAgentDirs)}`,
` Restore the missing agents.list entries or remove stale dirs after confirming they are no longer needed: ${shortenHomePath(path.join(stateDir, "agents"))}`,
].join("\n"),
);
}
const mainKey = resolveMainSessionKey(cfg); const mainEntry = store[mainKey]; if (mainEntry?.sessionId) { const transcriptPath = resolveSessionFilePath(
mainEntry.sessionId,
mainEntry,
sessionPathOpts,
); if (!existsFile(transcriptPath)) {
warnings.push(
`- Main session transcript missing (${shortenHomePath(transcriptPath)}). History will appear to reset.`,
);
} else { const lineCount = countJsonlLines(transcriptPath); if (lineCount <= 1) {
warnings.push(
`- Main session transcript has only ${lineCount} line. Session history may not be appending.`,
);
}
}
}
}
if (existsDir(sessionsDir)) { const referencedTranscriptPaths = new Set<string>(); for (const [, entry] of entries) { if (!entry?.sessionId) { continue;
} try {
referencedTranscriptPaths.add(
path.resolve(resolveSessionFilePath(entry.sessionId, entry, sessionPathOpts)),
);
} catch { // ignore invalid legacy paths
}
} const sessionDirEntries = fs.readdirSync(sessionsDir, { withFileTypes: true }); const orphanTranscriptPaths = sessionDirEntries
.filter((entry) => entry.isFile() && isPrimarySessionTranscriptFileName(entry.name))
.map((entry) => path.resolve(path.join(sessionsDir, entry.name)))
.filter((filePath) => !referencedTranscriptPaths.has(filePath)); if (orphanTranscriptPaths.length > 0 && !suppressOrphanTranscriptWarning) { const orphanCount = countLabel(orphanTranscriptPaths.length, "orphan transcript file"); const orphanPreview = formatFilePreview(orphanTranscriptPaths);
warnings.push(
[
`- Found ${orphanCount} in ${displaySessionsDir}.`, " These .jsonl files are no longer referenced by sessions.json, so they are not part of any active session history.", " Doctor can archive them safely by renaming each file to *.deleted.<timestamp>.",
` Examples: ${orphanPreview}`,
].join("\n"),
); const archiveOrphans = await prompter.confirmRuntimeRepair({
message: `Archive ${orphanCount} in ${displaySessionsDir}? This only renames them to *.deleted.<timestamp>.`,
initialValue: false,
}); if (archiveOrphans) {
let archived = 0; const archivedAt = formatSessionArchiveTimestamp(); for (const orphanPath of orphanTranscriptPaths) { const archivedPath = `${orphanPath}.deleted.${archivedAt}`; try {
fs.renameSync(orphanPath, archivedPath);
archived += 1;
} catch (err) {
warnings.push(
`- Failed to archive orphan transcript ${shortenHomePath(orphanPath)}: ${String(err)}`,
);
}
} if (archived > 0) {
changes.push(
`- Archived ${countLabel(archived, "orphan transcript file")} in ${displaySessionsDir} as .deleted timestamped backups.`,
);
}
}
}
}
if (warnings.length > 0) {
noteFn(warnings.join("\n"), "State integrity");
} if (changes.length > 0) {
noteFn(changes.join("\n"), "Doctor changes");
}
}
export function noteWorkspaceBackupTip(workspaceDir: string) { if (!existsDir(workspaceDir)) { return;
} const gitMarker = path.join(workspaceDir, ".git"); if (fs.existsSync(gitMarker)) { return;
}
note(
[ "- Tip: back up the workspace in a private git repo (GitHub or GitLab).", "- Keep ~/.openclaw out of git; it contains credentials and session history.", "- Details: /concepts/agent-workspace#git-backup-recommended",
].join("\n"), "Workspace",
);
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.