mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
Dreaming: harden atomic diary writes
This commit is contained in:
@@ -29,9 +29,15 @@ Docs: https://docs.openclaw.ai
|
||||
- Fireworks/FirePass: disable Kimi K2.5 Turbo reasoning output by forcing thinking off on the FirePass path and hardening the provider wrapper so hidden reasoning no longer leaks into visible replies. (#63607) Thanks @frankekn.
|
||||
- Sessions/model selection: preserve catalog-backed session model labels and keep already-qualified session model refs stable when catalog metadata is unavailable, so Control UI model selection survives reloads without bogus provider-prefixed values. (#61382) Thanks @Mule-ME.
|
||||
- Gateway/startup: keep WebSocket RPC available while channels and plugin sidecars start, hold `chat.history` unavailable until startup sidecars finish so synchronous history reads cannot stall startup (reported in #63450), refresh advertised gateway methods after deferred plugin reloads, and enforce the pre-auth WebSocket upgrade budget before the no-handler 503 path so upgrade floods cannot bypass connection limits during that window. (#63480) Thanks @neeravmakwana.
|
||||
<<<<<<< HEAD
|
||||
- Dreaming/cron: reconcile managed dreaming cron from the resolved gateway startup config so boot-time schedule recovery respects the configured cadence and timezone. (#63873) Thanks @mbelinky.
|
||||
||||||| parent of cc6ec8288a (Dreaming: harden atomic diary writes)
|
||||
- Dreaming/diary: add idempotent narrative subagent runs and atomic `DREAMS.md` writes so repeated sweeps do not double-run the same narrative request or partially rewrite the diary.
|
||||
=======
|
||||
>>>>>>> cc6ec8288a (Dreaming: harden atomic diary writes)
|
||||
- Gateway/tailscale: start Tailscale exposure and the gateway update check before awaiting channel and plugin sidecar startup so remote operators are not locked out when startup sidecars stall.
|
||||
- QQBot/streaming: make block streaming configurable per QQ bot account via `streaming.mode` (`"partial"` | `"off"`, default `"partial"`) instead of hardcoding it off, so responses can be delivered incrementally. (#63746)
|
||||
<<<<<<< HEAD
|
||||
- Dreaming/gateway: require `operator.admin` for persistent `/dreaming on|off` changes and treat missing gateway client scopes as unprivileged instead of silently allowing config writes. (#63872) Thanks @mbelinky.
|
||||
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
|
||||
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
|
||||
@@ -53,7 +59,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/dreaming: keep the Dreaming trace area contained and scrollable so overlays no longer cover tabs or blow out the page layout. (#63875) Thanks @mbelinky.
|
||||
- Dreaming/diary: add idempotent narrative subagent runs, preserve restrictive `DREAMS.md` permissions during atomic writes, and surface temp cleanup failures so repeated sweeps do not double-run the same narrative request or silently weaken diary safety. (#63876) Thanks @mbelinky.
|
||||
- Heartbeats/sessions: remove stale accumulated isolated heartbeat session keys when the next tick converges them back to the canonical sibling, so repaired sessions stop showing orphaned `:heartbeat:heartbeat` variants in session listings. (#59606) Thanks @rogerdigital.
|
||||
- Control UI/dreaming: keep the Dreaming trace area contained and scrollable so overlays no longer cover tabs or blow out the page layout.
|
||||
- Cron/Telegram: collapse isolated announce delivery to the final assistant-visible text only for Telegram targets, while preserving existing multi-message direct delivery semantics for other channels. (#63228) Thanks @welfo-beo.
|
||||
- Gateway/thread routing: preserve Slack, Telegram, and Mattermost thread-child delivery targets so bound subagent completion messages land in the originating thread instead of top-level channels. (#54840) Thanks @yzzymt.
|
||||
|
||||
|
||||
@@ -336,6 +336,44 @@ describe("appendNarrativeEntry", () => {
|
||||
expect(renameSpy).toHaveBeenCalledOnce();
|
||||
await expect(fs.readFile(dreamsPath, "utf-8")).resolves.toBe("# Existing\n");
|
||||
});
|
||||
|
||||
it("preserves restrictive dreams file permissions across atomic replace", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const dreamsPath = path.join(workspaceDir, "DREAMS.md");
|
||||
await fs.writeFile(dreamsPath, "# Existing\n", { encoding: "utf-8", mode: 0o600 });
|
||||
await fs.chmod(dreamsPath, 0o600);
|
||||
|
||||
await appendNarrativeEntry({
|
||||
workspaceDir,
|
||||
narrative: "Appended dream.",
|
||||
nowMs: Date.parse("2026-04-05T03:00:00Z"),
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
const stat = await fs.stat(dreamsPath);
|
||||
expect(stat.mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
it("surfaces temp cleanup failure after atomic replace error", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const dreamsPath = path.join(workspaceDir, "DREAMS.md");
|
||||
await fs.writeFile(dreamsPath, "# Existing\n", "utf-8");
|
||||
vi.spyOn(fs, "rename").mockRejectedValueOnce(
|
||||
Object.assign(new Error("replace failed"), { code: "ENOSPC" }),
|
||||
);
|
||||
vi.spyOn(fs, "rm").mockRejectedValueOnce(
|
||||
Object.assign(new Error("cleanup failed"), { code: "EACCES" }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
appendNarrativeEntry({
|
||||
workspaceDir,
|
||||
narrative: "Appended dream.",
|
||||
nowMs: Date.parse("2026-04-05T03:00:00Z"),
|
||||
timezone: "UTC",
|
||||
}),
|
||||
).rejects.toThrow("cleanup also failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateAndAppendDreamNarrative", () => {
|
||||
|
||||
@@ -278,12 +278,26 @@ async function assertSafeDreamsPath(dreamsPath: string): Promise<void> {
|
||||
|
||||
async function writeDreamsFileAtomic(dreamsPath: string, content: string): Promise<void> {
|
||||
await assertSafeDreamsPath(dreamsPath);
|
||||
const existing = await fs.stat(dreamsPath).catch((err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
const mode = existing?.mode ?? 0o600;
|
||||
const tempPath = `${dreamsPath}.${process.pid}.${Date.now()}.tmp`;
|
||||
await fs.writeFile(tempPath, content, { encoding: "utf-8", flag: "wx" });
|
||||
await fs.writeFile(tempPath, content, { encoding: "utf-8", flag: "wx", mode });
|
||||
await fs.chmod(tempPath, mode).catch(() => undefined);
|
||||
try {
|
||||
await fs.rename(tempPath, dreamsPath);
|
||||
await fs.chmod(dreamsPath, mode).catch(() => undefined);
|
||||
} catch (err) {
|
||||
await fs.rm(tempPath, { force: true }).catch(() => {});
|
||||
const cleanupError = await fs.rm(tempPath, { force: true }).catch((rmErr) => rmErr);
|
||||
if (cleanupError) {
|
||||
throw new Error(
|
||||
`Atomic DREAMS.md write failed (${formatErrorMessage(err)}); cleanup also failed (${formatErrorMessage(cleanupError)})`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user