From de81585e74b0aff8435ea49dc9141fb399d3cb1a Mon Sep 17 00:00:00 2001
From: blue-az
Date: Sun, 12 Jul 2026 15:17:42 +0200
Subject: [PATCH] feat(checkpoint): per-room opt-out for quick-checkpoint
auto-apply; auto-applied saves say so
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The default Checkpoint button approves a blocker-free proposal without
showing its content. This adds a room maintenance setting,
quickCheckpointAutoApply (default true, preserving current behaviour;
legacy settings files read as true), that routes the quick button to the
normal manual preview instead — reusing the existing fast-path-blocked
note so the user sees why. When auto-apply does run, the saved system
message now says the checkpoint was applied automatically. If the
preference cannot be read, the flow falls back to manual review — the
safe direction is showing the proposal, not silently applying it.
Co-Authored-By: Claude Fable 5
---
...sistent-room-maintenance-settings-smoke.ts | 25 ++++++++++-
apps/web-server/src/index.ts | 2 +-
.../persistent-room-maintenance-settings.ts | 16 ++++++-
apps/web-ui/src/App.tsx | 16 ++++++-
.../src/components/RoomMaintenanceSection.tsx | 43 +++++++++++++++++++
.../src/persistent-room-management-api.ts | 3 +-
6 files changed, 100 insertions(+), 5 deletions(-)
diff --git a/apps/web-server/scripts/persistent-room-maintenance-settings-smoke.ts b/apps/web-server/scripts/persistent-room-maintenance-settings-smoke.ts
index 574c7c5..b5ddb24 100644
--- a/apps/web-server/scripts/persistent-room-maintenance-settings-smoke.ts
+++ b/apps/web-server/scripts/persistent-room-maintenance-settings-smoke.ts
@@ -18,9 +18,10 @@ function assert(condition: unknown, message: string): asserts condition {
}
try {
- // Default: off, 20k budget, no file created by reading.
+ // Default: off, quick-checkpoint auto-apply on, 20k budget, no file created by reading.
const initial = readPersistentRoomMaintenanceSettings(agentId);
assert(initial.fastPathSecondApproval === false, "default fast-path must be off");
+ assert(initial.quickCheckpointAutoApply === true, "default quick-checkpoint auto-apply must be on");
assert(initial.memoryBudgetTokens === 20_000, "default memory budget must be 20k tokens");
assert(!fs.existsSync(persistentRoomMaintenanceSettingsPath(agentId)), "read must not create the settings file");
@@ -61,6 +62,20 @@ try {
writePersistentRoomMaintenanceSettings(agentId, { fastPathSecondApproval: false });
assert(readPersistentRoomMaintenanceSettings(agentId).fastPathSecondApproval === false, "toggle off should persist");
+ // Quick-checkpoint auto-apply: off round-trips, other writes preserve it,
+ // and settings files written before the field existed keep auto-apply on.
+ const quickOff = writePersistentRoomMaintenanceSettings(agentId, { quickCheckpointAutoApply: false });
+ assert(quickOff.quickCheckpointAutoApply === false, "quick auto-apply off should persist");
+ assert(quickOff.fastPathSecondApproval === false, "quick auto-apply write should preserve the fast-path toggle");
+ assert(readPersistentRoomMaintenanceSettings(agentId).quickCheckpointAutoApply === false, "reread should see quick auto-apply off");
+ assert(writePersistentRoomMaintenanceSettings(agentId, { memoryBudgetTokens: 25_000 }).quickCheckpointAutoApply === false, "budget-only write should preserve quick auto-apply");
+ writePersistentRoomMaintenanceSettings(agentId, { quickCheckpointAutoApply: true });
+ assert(readPersistentRoomMaintenanceSettings(agentId).quickCheckpointAutoApply === true, "quick auto-apply back on should persist");
+ const legacy = JSON.parse(fs.readFileSync(persistentRoomMaintenanceSettingsPath(agentId), "utf-8"));
+ delete legacy.quickCheckpointAutoApply;
+ fs.writeFileSync(persistentRoomMaintenanceSettingsPath(agentId), JSON.stringify(legacy, null, 2) + "\n", "utf-8");
+ assert(readPersistentRoomMaintenanceSettings(agentId).quickCheckpointAutoApply === true, "legacy settings without the field should keep auto-apply on");
+
// Validation.
let threw = false;
try {
@@ -69,6 +84,13 @@ try {
threw = /must be a boolean/.test((error as Error).message);
}
assert(threw, "non-boolean input should be rejected");
+ let quickThrew = false;
+ try {
+ writePersistentRoomMaintenanceSettings(agentId, { quickCheckpointAutoApply: "yes" as unknown as boolean });
+ } catch (error) {
+ quickThrew = /quickCheckpointAutoApply must be a boolean/.test((error as Error).message);
+ }
+ assert(quickThrew, "non-boolean quick auto-apply should be rejected");
let threwId = false;
try {
readPersistentRoomMaintenanceSettings("../escape");
@@ -80,6 +102,7 @@ try {
// Corrupt file falls back to defaults.
fs.writeFileSync(persistentRoomMaintenanceSettingsPath(agentId), "not json", "utf-8");
assert(readPersistentRoomMaintenanceSettings(agentId).fastPathSecondApproval === false, "corrupt settings should fall back to default off");
+ assert(readPersistentRoomMaintenanceSettings(agentId).quickCheckpointAutoApply === true, "corrupt settings should fall back to auto-apply on");
fs.rmSync(root, { recursive: true, force: true });
console.log("persistent-room maintenance settings smoke passed");
diff --git a/apps/web-server/src/index.ts b/apps/web-server/src/index.ts
index c51595b..a9499f1 100644
--- a/apps/web-server/src/index.ts
+++ b/apps/web-server/src/index.ts
@@ -1162,7 +1162,7 @@ app.put("/api/persistent-agents/:id/maintenance-settings", async (req, reply) =>
try {
const status = getUsablePersistentAgentStatusForNormalUse(idRaw);
const body = (req.body ?? {}) as any;
- const settings = writePersistentRoomMaintenanceSettings(status.id, { fastPathSecondApproval: body.fastPathSecondApproval, memoryBudgetTokens: body.memoryBudgetTokens });
+ const settings = writePersistentRoomMaintenanceSettings(status.id, { fastPathSecondApproval: body.fastPathSecondApproval, quickCheckpointAutoApply: body.quickCheckpointAutoApply, memoryBudgetTokens: body.memoryBudgetTokens });
return { agentId: status.id, settings };
} catch (e) {
return persistentAgentNormalUseErrorReply(reply, e);
diff --git a/apps/web-server/src/persistent-room-maintenance-settings.ts b/apps/web-server/src/persistent-room-maintenance-settings.ts
index e8f5b79..a109292 100644
--- a/apps/web-server/src/persistent-room-maintenance-settings.ts
+++ b/apps/web-server/src/persistent-room-maintenance-settings.ts
@@ -12,6 +12,13 @@ import { DEFAULT_PERSISTENT_ROOM_AGENTS_ROOT, persistentAgentRootPath } from "./
* and the server-side propose/approve split is unchanged — this file only
* stores the preference.
*
+ * `quickCheckpointAutoApply` governs the default Checkpoint button: when true
+ * (the default, matching historical behaviour) a blocker-free proposal is
+ * approved immediately without showing the preview; when false the quick
+ * button still generates the proposal but always falls back to the manual
+ * preview for approval. Proposals with blockers fall back to the preview
+ * either way, and the server-side propose/approve split is unchanged.
+ *
* `memoryBudgetTokens` is the room's advisory memory budget: the size the
* whole L1b should stay near. It never gates anything — it drives the
* settings meter, the room-card nudge, and a target line in the absorb /
@@ -21,6 +28,7 @@ import { DEFAULT_PERSISTENT_ROOM_AGENTS_ROOT, persistentAgentRootPath } from "./
export interface PersistentRoomMaintenanceSettings {
schemaVersion: 1;
fastPathSecondApproval: boolean;
+ quickCheckpointAutoApply: boolean;
memoryBudgetTokens: number;
updatedAt: string;
}
@@ -41,6 +49,7 @@ function clampMemoryBudgetTokens(value: unknown): number {
const DEFAULT_SETTINGS: PersistentRoomMaintenanceSettings = {
schemaVersion: 1,
fastPathSecondApproval: false,
+ quickCheckpointAutoApply: true,
memoryBudgetTokens: MEMORY_BUDGET_DEFAULT_TOKENS,
updatedAt: "",
};
@@ -65,6 +74,9 @@ export function readPersistentRoomMaintenanceSettings(agentIdRaw: string, option
return {
schemaVersion: 1,
fastPathSecondApproval: raw.fastPathSecondApproval === true,
+ // Settings files written before this field existed must keep the
+ // historical auto-apply behaviour, so only an explicit false disables.
+ quickCheckpointAutoApply: raw.quickCheckpointAutoApply !== false,
memoryBudgetTokens: clampMemoryBudgetTokens(raw.memoryBudgetTokens),
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : "",
};
@@ -73,14 +85,16 @@ export function readPersistentRoomMaintenanceSettings(agentIdRaw: string, option
}
}
-export function writePersistentRoomMaintenanceSettings(agentIdRaw: string, input: { fastPathSecondApproval?: unknown; memoryBudgetTokens?: unknown }, options: PersistentRoomMaintenanceSettingsStorageOptions = {}, now = new Date()): PersistentRoomMaintenanceSettings {
+export function writePersistentRoomMaintenanceSettings(agentIdRaw: string, input: { fastPathSecondApproval?: unknown; quickCheckpointAutoApply?: unknown; memoryBudgetTokens?: unknown }, options: PersistentRoomMaintenanceSettingsStorageOptions = {}, now = new Date()): PersistentRoomMaintenanceSettings {
if (input?.fastPathSecondApproval !== undefined && typeof input.fastPathSecondApproval !== "boolean") throw new Error("fastPathSecondApproval must be a boolean");
+ if (input?.quickCheckpointAutoApply !== undefined && typeof input.quickCheckpointAutoApply !== "boolean") throw new Error("quickCheckpointAutoApply must be a boolean");
if (input?.memoryBudgetTokens !== undefined && (typeof input.memoryBudgetTokens !== "number" || !Number.isFinite(input.memoryBudgetTokens))) throw new Error("memoryBudgetTokens must be a number");
const current = readPersistentRoomMaintenanceSettings(agentIdRaw, options);
const settingsPath = persistentRoomMaintenanceSettingsPath(agentIdRaw, options);
const settings: PersistentRoomMaintenanceSettings = {
schemaVersion: 1,
fastPathSecondApproval: input?.fastPathSecondApproval !== undefined ? input.fastPathSecondApproval as boolean : current.fastPathSecondApproval,
+ quickCheckpointAutoApply: input?.quickCheckpointAutoApply !== undefined ? input.quickCheckpointAutoApply as boolean : current.quickCheckpointAutoApply,
memoryBudgetTokens: input?.memoryBudgetTokens !== undefined ? clampMemoryBudgetTokens(input.memoryBudgetTokens) : current.memoryBudgetTokens,
updatedAt: now.toISOString(),
};
diff --git a/apps/web-ui/src/App.tsx b/apps/web-ui/src/App.tsx
index 87f1426..d85ab68 100644
--- a/apps/web-ui/src/App.tsx
+++ b/apps/web-ui/src/App.tsx
@@ -3246,6 +3246,10 @@ export function App() {
setCheckpointQuickBlockedReasons(blockers);
return;
}
+ if (!(await roomQuickCheckpointAutoApplyEnabled(targetChat.agentId))) {
+ setCheckpointQuickBlockedReasons(["automatic apply is turned off for this room"]);
+ return;
+ }
setCheckpointApprovalLoading(true);
try {
const approvedRecentContext = buildApprovedRecentContextMarkdown(proposal, {
@@ -3257,7 +3261,7 @@ export function App() {
await bindToApprovedCheckpointRuntime(approval, targetChat);
void refreshPersistentAgentStatus();
resetCheckpointInput();
- const savedNote = approval.warnings.length > 0 ? `Checkpoint saved to memory. ${approval.warnings.join(" ")}` : "Checkpoint saved to memory.";
+ const savedNote = approval.warnings.length > 0 ? `Checkpoint saved to memory automatically. ${approval.warnings.join(" ")}` : "Checkpoint saved to memory automatically.";
setItems((s) => [...s, { kind: "system", id: nid(), text: savedNote }]);
} catch (e) {
setCheckpointApprovalError(`The checkpoint could not save automatically. Review and approve it manually. ${(e as Error).message}`);
@@ -3806,6 +3810,16 @@ export function App() {
}
}
+ async function roomQuickCheckpointAutoApplyEnabled(agentId: PersistentAgentId): Promise {
+ try {
+ return (await fetchPersistentRoomMaintenanceSettings(agentId)).settings.quickCheckpointAutoApply !== false;
+ } catch {
+ // When the preference cannot be read, fall back to manual review —
+ // the safe direction is showing the proposal, not silently applying it.
+ return false;
+ }
+ }
+
/**
* Fast path: when the room opts in and the proposal is warning-free, apply
* it immediately instead of showing the second approval screen. Returns
diff --git a/apps/web-ui/src/components/RoomMaintenanceSection.tsx b/apps/web-ui/src/components/RoomMaintenanceSection.tsx
index 6e536aa..6e26be8 100644
--- a/apps/web-ui/src/components/RoomMaintenanceSection.tsx
+++ b/apps/web-ui/src/components/RoomMaintenanceSection.tsx
@@ -14,6 +14,7 @@ function fmtTokensK(value: number): string {
export function RoomMaintenanceSection({ status }: { status: PersistentAgentStatus }) {
const [fastPath, setFastPath] = useState(null);
+ const [quickAutoApply, setQuickAutoApply] = useState(null);
const [budget, setBudget] = useState(null);
const [savedBudget, setSavedBudget] = useState(null);
const [saving, setSaving] = useState(false);
@@ -23,6 +24,7 @@ export function RoomMaintenanceSection({ status }: { status: PersistentAgentStat
useEffect(() => {
let cancelled = false;
setFastPath(null);
+ setQuickAutoApply(null);
setBudget(null);
setSavedBudget(null);
setError(null);
@@ -30,6 +32,7 @@ export function RoomMaintenanceSection({ status }: { status: PersistentAgentStat
.then((response) => {
if (cancelled) return;
setFastPath(response.settings.fastPathSecondApproval);
+ setQuickAutoApply(response.settings.quickCheckpointAutoApply);
setBudget(response.settings.memoryBudgetTokens);
setSavedBudget(response.settings.memoryBudgetTokens);
})
@@ -78,6 +81,23 @@ export function RoomMaintenanceSection({ status }: { status: PersistentAgentStat
}
}
+ async function toggleQuickAutoApply(next: boolean) {
+ if (saving || quickAutoApply === null) return;
+ setSaving(true);
+ setError(null);
+ const previous = quickAutoApply;
+ setQuickAutoApply(next);
+ try {
+ const response = await updatePersistentRoomMaintenanceSettings(status.id, { quickCheckpointAutoApply: next });
+ setQuickAutoApply(response.settings.quickCheckpointAutoApply);
+ } catch (e) {
+ setQuickAutoApply(previous);
+ setError((e as Error).message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
const currentMemoryTokens = status.promptBudget?.l1bEstimatedTokens ?? null;
const usagePercent = budget !== null && currentMemoryTokens !== null ? Math.round((currentMemoryTokens / budget) * 100) : null;
const overBudget = usagePercent !== null && usagePercent > 100;
@@ -106,6 +126,29 @@ export function RoomMaintenanceSection({ status }: { status: PersistentAgentStat
an audit record.
+
+
Save blocker-free Checkpoint proposals without showing the preview.
+
+ How it works
+
+ The Checkpoint button drafts a memory proposal and, when nothing deterministic
+ blocks it, saves it without showing you the content first. Turn this off to
+ always review the proposed entry before it becomes memory. Proposals with
+ blockers — trimmed transcripts or incomplete drafts — always come back for
+ review either way, and every save still archives the previous memory and writes
+ an audit record.
+