FN-5939: expose operational log retention setting
Expose operational log retention as a project setting in the dashboard. - add an Operational log retention selector to the Project General settings section with supported retention options - validate operationalLogRetentionDays in the settings API and cover accepted and rejected values in tests - document the constrained retention values and assert project-scope/default parity for the setting Files changed: docs/settings-reference.md | 2 +- packages/core/src/__tests__/settings-parity.test.ts | 7 ++++ packages/dashboard/app/components/SettingsModal.tsx | 47 ++++++++++------------ packages/dashboard/app/components/__tests__/SettingsModal.test.tsx | 8 ++++ packages/dashboard/src/__tests__/routes-settings.test.ts | 21 ++++++++++ packages/dashboard/src/routes/register-settings-memory-routes.ts | 10 +++++ 6 files changed, 69 insertions(+), 26 deletions(-) Fusion-Task-Id: FN-5939 Fusion-Task-Lineage: 2148dd88-1cef-4c6d-9696-31148fce97d3
This commit is contained in:
@@ -430,7 +430,7 @@ Default notes:
|
||||
| `showQuickChatFAB` | `boolean` | `false` | Show floating quick-chat button (chat remains available via More menu). |
|
||||
| `chatAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-cleanup retention window for idle chat sessions and chat rooms. `0` is off (default). When enabled, periodic self-healing maintenance deletes rows with `updatedAt` older than the configured day window. |
|
||||
| `mailAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-prune retention window for inbox/outbox mail messages. `0` is off (default). When enabled, periodic self-healing maintenance deletes `messages` rows where `updatedAt < cutoff` for the configured day window. Suggested setting: `7`. |
|
||||
| `operationalLogRetentionDays` | `number` | `30` | Retention window for SQLite operational-log tables (`activityLog`, `runAuditEvents`, `agentHeartbeats`). Periodic maintenance prunes rows older than this many days using each row's `timestamp`. Set `0` to disable pruning. |
|
||||
| `operationalLogRetentionDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `30` | Retention window for SQLite operational-log tables (`activityLog`, `runAuditEvents`, `agentHeartbeats`). `0` is off. Lower values mean Reliability metrics/charts and the Activity feed will not show history older than the configured window; per-task task detail history is unaffected. Periodic maintenance prunes rows older than this many days using each row's `timestamp`. |
|
||||
| `agentLogFileRetentionDays` | `number` | `0` | Retention window for per-task `.fusion/tasks/{ID}/agent-log.jsonl` files after a task is soft-deleted or archived. Periodic maintenance removes JSONL entries older than this many days; active tasks are never pruned. Set `0` to disable pruning. |
|
||||
| `chatRoomRecentVerbatimMessages` | `number` | `25` | Number of newest chat-room messages kept verbatim in responder context before older entries are compacted (about 2× prior default history). |
|
||||
| `chatRoomCompactionFetchLimit` | `number` | `200` | Upper bound on room messages fetched for transcript compaction per responder turn (raised to support larger retained context windows). |
|
||||
|
||||
@@ -117,6 +117,13 @@ describe("settings key parity", () => {
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("mailAutoCleanupDays");
|
||||
});
|
||||
|
||||
it("defaults operationalLogRetentionDays to 30 and keeps it project-scoped", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.operationalLogRetentionDays).toBe(30);
|
||||
expect(isProjectSettingsKey("operationalLogRetentionDays")).toBe(true);
|
||||
expect(isGlobalSettingsKey("operationalLogRetentionDays")).toBe(false);
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("operationalLogRetentionDays");
|
||||
});
|
||||
|
||||
it("keeps heartbeatScopeDiscipline project-scoped with strict default", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.heartbeatScopeDiscipline).toBe("strict");
|
||||
expect(isProjectSettingsKey("heartbeatScopeDiscipline")).toBe(true);
|
||||
|
||||
@@ -2451,6 +2451,28 @@ export function SettingsModal({
|
||||
</select>
|
||||
<small>Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="operationalLogRetentionDays">Operational log retention</label>
|
||||
<select
|
||||
id="operationalLogRetentionDays"
|
||||
className="select"
|
||||
value={form.operationalLogRetentionDays ?? 30}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, operationalLogRetentionDays: Number(e.target.value) || 0 }))
|
||||
}
|
||||
>
|
||||
<option value={0}>Off</option>
|
||||
<option value={7}>7 days</option>
|
||||
<option value={14}>14 days</option>
|
||||
<option value={30}>30 days</option>
|
||||
<option value={60}>60 days</option>
|
||||
<option value={90}>90 days</option>
|
||||
</select>
|
||||
<small>
|
||||
Lowering this window means Reliability metrics/charts and the Activity feed will not show history older
|
||||
than the selected range. Per-task task detail history is unaffected. Default: 30 days.
|
||||
</small>
|
||||
</div>
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">Chat Rooms</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="chatRoomRecentVerbatimMessages">Recent verbatim room messages</label>
|
||||
@@ -6108,31 +6130,6 @@ export function SettingsModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">Database Maintenance</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="operationalLogRetentionDays">Operational log retention</label>
|
||||
<select
|
||||
id="operationalLogRetentionDays"
|
||||
className="select"
|
||||
value={form.operationalLogRetentionDays ?? 0}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, operationalLogRetentionDays: Number(e.target.value) || 0 }))
|
||||
}
|
||||
>
|
||||
<option value={0}>Off</option>
|
||||
<option value={30}>30 days</option>
|
||||
<option value={60}>60 days</option>
|
||||
<option value={90}>90 days</option>
|
||||
<option value={180}>180 days</option>
|
||||
<option value={365}>365 days</option>
|
||||
</select>
|
||||
<small>
|
||||
Prune append-only operational logs (activity log, agent logs, run audit, heartbeats) older than this
|
||||
many days during periodic maintenance. Keeps the database from growing without bound — large databases
|
||||
are slower to checkpoint and more prone to corruption. Default: 30 days.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h4 className="settings-section-heading">Memory Backups</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupEnabled" className="checkbox-label">
|
||||
|
||||
@@ -997,6 +997,14 @@ describe("SettingsModal", () => {
|
||||
scope: "project",
|
||||
expectedKey: "chatAutoCleanupDays",
|
||||
},
|
||||
{
|
||||
section: "Project General",
|
||||
label: "Operational log retention",
|
||||
kind: "select",
|
||||
value: 7,
|
||||
scope: "project",
|
||||
expectedKey: "operationalLogRetentionDays",
|
||||
},
|
||||
])("persists $expectedKey through the expected settings scope", async (input) => {
|
||||
await expectSettingPersists(input);
|
||||
});
|
||||
|
||||
@@ -810,6 +810,27 @@ describe("PUT /settings", () => {
|
||||
expect(store.updateSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts allowed operationalLogRetentionDays values", async () => {
|
||||
for (const value of [7, 0]) {
|
||||
const res = await REQUEST(buildApp(), "PUT", "/api/settings", JSON.stringify({ operationalLogRetentionDays: value }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({ operationalLogRetentionDays: value });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid operationalLogRetentionDays values", async () => {
|
||||
const res = await REQUEST(buildApp(), "PUT", "/api/settings", JSON.stringify({ operationalLogRetentionDays: 45 }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("operationalLogRetentionDays");
|
||||
expect(store.updateSettings).not.toHaveBeenCalledWith({ operationalLogRetentionDays: 45 });
|
||||
});
|
||||
|
||||
it("accepts partial remoteAccess patches and GET /settings returns merged sibling branches", async () => {
|
||||
const mergedRemoteAccess = {
|
||||
enabled: true,
|
||||
|
||||
@@ -614,6 +614,16 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
||||
throw badRequest("doneAutoArchiveDays must be an integer between 0 and 3650");
|
||||
}
|
||||
}
|
||||
const operationalLogRetentionDays = clientSettings.operationalLogRetentionDays;
|
||||
if (operationalLogRetentionDays !== undefined && operationalLogRetentionDays !== null) {
|
||||
if (
|
||||
typeof operationalLogRetentionDays !== "number"
|
||||
|| !Number.isInteger(operationalLogRetentionDays)
|
||||
|| ![0, 7, 14, 30, 60, 90].includes(operationalLogRetentionDays)
|
||||
) {
|
||||
throw badRequest("operationalLogRetentionDays must be one of: 0, 7, 14, 30, 60, 90");
|
||||
}
|
||||
}
|
||||
if (
|
||||
clientSettings.archiveAgentLogMode !== undefined &&
|
||||
!["none", "compact", "full"].includes(clientSettings.archiveAgentLogMode)
|
||||
|
||||
Reference in New Issue
Block a user