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
Save blocker-free Checkpoint proposals without showing the preview.
++ 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. +
+