feat(FN-5374): raise room transcript defaults and align compaction settings

Raised room transcript defaults (`messagesBefore` and `daysBefore`) in the core settings schema and updated project-level setting defaults, with corresponding documentation refresh in the settings reference. Added full test coverage for room compaction defaults, pinned room default settings in Setti

Fusion-Task-Id: FN-5374
This commit is contained in:
Fusion (runfusion.ai)
2026-05-21 07:00:48 -07:00
committed by gsxdsm
parent e12adeb3fa
commit 5f9f777630
10 changed files with 56 additions and 32 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Increase default group chat context retention (`chatRoomRecentVerbatimMessages` 12 → 25, `chatRoomCompactionFetchLimit` 80 → 200, `chatRoomSummaryMaxChars` 1500 → 3000) and raise the room transcript cap from 8KB to 20KB.

View File

@@ -425,9 +425,9 @@ Default notes:
| `showQuickChatFAB` | `boolean` | `false` | Show floating quick-chat button (chat remains available via More menu). | | `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. | | `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`. | | `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`. |
| `chatRoomRecentVerbatimMessages` | `number` | `12` | Number of newest chat-room messages kept verbatim in responder context before older entries are compacted. | | `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` | `80` | Upper bound on room messages fetched for transcript compaction per responder turn. | | `chatRoomCompactionFetchLimit` | `number` | `200` | Upper bound on room messages fetched for transcript compaction per responder turn (raised to support larger retained context windows). |
| `chatRoomSummaryMaxChars` | `number` | `1500` | Hard cap for the synthesized “Earlier room context” summary block. | | `chatRoomSummaryMaxChars` | `number` | `3000` | Hard cap for the synthesized “Earlier room context” summary block (about 2× the prior summary budget). |
| `researchSettings` | `ResearchProjectSettings` | `{ enabled: true, searchProvider: undefined, synthesisProvider: undefined, synthesisModelId: undefined, enabledSources: { webSearch: true, pageFetch: true, github: false, localDocs: true, llmSynthesis: true }, limits: { maxConcurrentRuns: 3, maxSourcesPerRun: 20, maxDurationMs: 300000, requestTimeoutMs: 30000 } }` | Project-specific Research enablement/overrides. Resolved together with `researchGlobalDefaults` via `resolveResearchSettings()`. | | `researchSettings` | `ResearchProjectSettings` | `{ enabled: true, searchProvider: undefined, synthesisProvider: undefined, synthesisModelId: undefined, enabledSources: { webSearch: true, pageFetch: true, github: false, localDocs: true, llmSynthesis: true }, limits: { maxConcurrentRuns: 3, maxSourcesPerRun: 20, maxDurationMs: 300000, requestTimeoutMs: 30000 } }` | Project-specific Research enablement/overrides. Resolved together with `researchGlobalDefaults` via `resolveResearchSettings()`. |
| `researchEnabled` | `boolean` | `undefined` | Enable or disable research for this project. **Deprecated:** prefer `researchSettings.enabled`. | | `researchEnabled` | `boolean` | `undefined` | Enable or disable research for this project. **Deprecated:** prefer `researchSettings.enabled`. |
| `researchMaxConcurrentRuns` | `number` | `undefined` | Project-level max concurrent research runs. | | `researchMaxConcurrentRuns` | `number` | `undefined` | Project-level max concurrent research runs. |

View File

@@ -96,6 +96,18 @@ describe("settings key parity", () => {
expect(isGlobalSettingsKey("chatAutoCleanupDays")).toBe(false); expect(isGlobalSettingsKey("chatAutoCleanupDays")).toBe(false);
}); });
it("keeps room compaction defaults project-scoped with expanded retention", () => {
expect(DEFAULT_PROJECT_SETTINGS.chatRoomRecentVerbatimMessages).toBe(25);
expect(DEFAULT_PROJECT_SETTINGS.chatRoomCompactionFetchLimit).toBe(200);
expect(DEFAULT_PROJECT_SETTINGS.chatRoomSummaryMaxChars).toBe(3_000);
expect(isProjectSettingsKey("chatRoomRecentVerbatimMessages")).toBe(true);
expect(isProjectSettingsKey("chatRoomCompactionFetchLimit")).toBe(true);
expect(isProjectSettingsKey("chatRoomSummaryMaxChars")).toBe(true);
expect(isGlobalSettingsKey("chatRoomRecentVerbatimMessages")).toBe(false);
expect(isGlobalSettingsKey("chatRoomCompactionFetchLimit")).toBe(false);
expect(isGlobalSettingsKey("chatRoomSummaryMaxChars")).toBe(false);
});
it("defaults mailAutoCleanupDays to off and keeps it project-scoped", () => { it("defaults mailAutoCleanupDays to off and keeps it project-scoped", () => {
expect(DEFAULT_PROJECT_SETTINGS.mailAutoCleanupDays).toBe(0); expect(DEFAULT_PROJECT_SETTINGS.mailAutoCleanupDays).toBe(0);
expect(isProjectSettingsKey("mailAutoCleanupDays")).toBe(true); expect(isProjectSettingsKey("mailAutoCleanupDays")).toBe(true);

View File

@@ -389,9 +389,9 @@ export const DEFAULT_PROJECT_SETTINGS = {
showQuickChatFAB: false, showQuickChatFAB: false,
chatAutoCleanupDays: 0, chatAutoCleanupDays: 0,
mailAutoCleanupDays: 0, mailAutoCleanupDays: 0,
chatRoomRecentVerbatimMessages: 12, chatRoomRecentVerbatimMessages: 25,
chatRoomCompactionFetchLimit: 80, chatRoomCompactionFetchLimit: 200,
chatRoomSummaryMaxChars: 1_500, chatRoomSummaryMaxChars: 3_000,
researchSettings: { researchSettings: {
enabled: true, enabled: true,
searchProvider: undefined, searchProvider: undefined,

View File

@@ -2385,13 +2385,13 @@ export function SettingsModal({
type="number" type="number"
min="1" min="1"
className="input" className="input"
placeholder="12" placeholder="25"
value={form.chatRoomRecentVerbatimMessages ?? ""} value={form.chatRoomRecentVerbatimMessages ?? ""}
onChange={(e) => onChange={(e) =>
setForm((f) => ({ ...f, chatRoomRecentVerbatimMessages: Number(e.target.value) || undefined })) setForm((f) => ({ ...f, chatRoomRecentVerbatimMessages: Number(e.target.value) || undefined }))
} }
/> />
<small>Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 12.</small> <small>Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="chatRoomCompactionFetchLimit">Room compaction fetch limit</label> <label htmlFor="chatRoomCompactionFetchLimit">Room compaction fetch limit</label>
@@ -2400,13 +2400,13 @@ export function SettingsModal({
type="number" type="number"
min="1" min="1"
className="input" className="input"
placeholder="80" placeholder="200"
value={form.chatRoomCompactionFetchLimit ?? ""} value={form.chatRoomCompactionFetchLimit ?? ""}
onChange={(e) => onChange={(e) =>
setForm((f) => ({ ...f, chatRoomCompactionFetchLimit: Number(e.target.value) || undefined })) setForm((f) => ({ ...f, chatRoomCompactionFetchLimit: Number(e.target.value) || undefined }))
} }
/> />
<small>Upper bound on messages fetched from the room store for compaction consideration. Default: 80.</small> <small>Upper bound on messages fetched from the room store for compaction consideration. Default: 200.</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="chatRoomSummaryMaxChars">Room summary max characters</label> <label htmlFor="chatRoomSummaryMaxChars">Room summary max characters</label>
@@ -2415,13 +2415,13 @@ export function SettingsModal({
type="number" type="number"
min="200" min="200"
className="input" className="input"
placeholder="1500" placeholder="3000"
value={form.chatRoomSummaryMaxChars ?? ""} value={form.chatRoomSummaryMaxChars ?? ""}
onChange={(e) => onChange={(e) =>
setForm((f) => ({ ...f, chatRoomSummaryMaxChars: Number(e.target.value) || undefined })) setForm((f) => ({ ...f, chatRoomSummaryMaxChars: Number(e.target.value) || undefined }))
} }
/> />
<small>Hard cap on the synthesized "Earlier room context" summary block. Default: 1500.</small> <small>Hard cap on the synthesized "Earlier room context" summary block. Default: 3000.</small>
</div> </div>
<h4 className="settings-section-heading settings-section-heading--spaced">Capacity Risk Banner</h4> <h4 className="settings-section-heading settings-section-heading--spaced">Capacity Risk Banner</h4>
<div className="form-group"> <div className="form-group">

View File

@@ -1032,9 +1032,9 @@ describe("SettingsModal", () => {
const fetchLimitInput = screen.getByLabelText("Room compaction fetch limit") as HTMLInputElement; const fetchLimitInput = screen.getByLabelText("Room compaction fetch limit") as HTMLInputElement;
const summaryMaxInput = screen.getByLabelText("Room summary max characters") as HTMLInputElement; const summaryMaxInput = screen.getByLabelText("Room summary max characters") as HTMLInputElement;
expect(recentInput.placeholder).toBe("12"); expect(recentInput.placeholder).toBe("25");
expect(fetchLimitInput.placeholder).toBe("80"); expect(fetchLimitInput.placeholder).toBe("200");
expect(summaryMaxInput.placeholder).toBe("1500"); expect(summaryMaxInput.placeholder).toBe("3000");
await userEvent.type(recentInput, "7"); await userEvent.type(recentInput, "7");
await userEvent.type(fetchLimitInput, "60"); await userEvent.type(fetchLimitInput, "60");

View File

@@ -29,7 +29,7 @@ describe("buildCompactedRoomTranscript", () => {
expect(transcript.match(/\[LATEST USER MESSAGE — ANSWER THIS\]/g)).toHaveLength(1); expect(transcript.match(/\[LATEST USER MESSAGE — ANSWER THIS\]/g)).toHaveLength(1);
}); });
it("prepends a compacted summary and keeps the last 12 messages verbatim", () => { it("prepends a compacted summary and keeps the last 25 messages verbatim by default", () => {
const messages = Array.from({ length: 30 }, (_, index) => { const messages = Array.from({ length: 30 }, (_, index) => {
const olderUserLengths = [40, 80, 120, 160, 200, 220, 60, 70, 90]; const olderUserLengths = [40, 80, 120, 160, 200, 220, 60, 70, 90];
const content = index < 18 && index % 2 === 0 const content = index < 18 && index % 2 === 0
@@ -42,7 +42,7 @@ describe("buildCompactedRoomTranscript", () => {
const transcript = buildCompactedRoomTranscript(messages, latestUserMessageId); const transcript = buildCompactedRoomTranscript(messages, latestUserMessageId);
expect(transcript).toContain("## Earlier room context (compacted)"); expect(transcript).toContain("## Earlier room context (compacted)");
expect(transcript).toContain("- Span: 18 messages from 2026-01-01T00:00:00.000Z to 2026-01-01T00:00:17.000Z"); expect(transcript).toContain("- Span: 5 messages from 2026-01-01T00:00:00.000Z to 2026-01-01T00:00:04.000Z");
expect(transcript).toContain("- Participants: User, Agent agent-a"); expect(transcript).toContain("- Participants: User, Agent agent-a");
const [summaryBlock] = transcript.split("\n\n"); const [summaryBlock] = transcript.split("\n\n");
const highlightLines = summaryBlock.split("\n").filter((line) => line.startsWith(" - ")); const highlightLines = summaryBlock.split("\n").filter((line) => line.startsWith(" - "));
@@ -50,9 +50,10 @@ describe("buildCompactedRoomTranscript", () => {
const highlightTimestamps = highlightLines.map((line) => line.match(/\[(.*?)\]/)?.[1] ?? ""); const highlightTimestamps = highlightLines.map((line) => line.match(/\[(.*?)\]/)?.[1] ?? "");
expect(highlightTimestamps).toEqual([...highlightTimestamps].sort()); expect(highlightTimestamps).toEqual([...highlightTimestamps].sort());
for (let index = 18; index < 30; index += 1) { for (let index = 5; index < 30; index += 1) {
expect(transcript).toContain(`- [2026-01-01T00:00:${String(index).padStart(2, "0")}.000Z]`); expect(transcript).toContain(`- [2026-01-01T00:00:${String(index).padStart(2, "0")}.000Z]`);
} }
expect(transcript).not.toContain("- [2026-01-01T00:00:04.000Z] (user) User:");
expect(transcript).toContain("(user) User: message-28 [LATEST USER MESSAGE — ANSWER THIS]"); expect(transcript).toContain("(user) User: message-28 [LATEST USER MESSAGE — ANSWER THIS]");
}); });
@@ -65,7 +66,7 @@ describe("buildCompactedRoomTranscript", () => {
const transcript = buildCompactedRoomTranscript(messages, "msg-29"); const transcript = buildCompactedRoomTranscript(messages, "msg-29");
expect(transcript.length).toBeLessThanOrEqual(8000); expect(transcript.length).toBeLessThanOrEqual(20000);
expect(transcript.match(/\[LATEST USER MESSAGE — ANSWER THIS\]/g)).toHaveLength(1); expect(transcript.match(/\[LATEST USER MESSAGE — ANSWER THIS\]/g)).toHaveLength(1);
expect(transcript).toContain("message-29-"); expect(transcript).toContain("message-29-");
}); });
@@ -81,7 +82,7 @@ describe("buildCompactedRoomTranscript", () => {
content: `recent-${index}`, content: `recent-${index}`,
})); }));
const transcript = buildCompactedRoomTranscript([...olderMessages, ...recentMessages], "msg-29"); const transcript = buildCompactedRoomTranscript([...olderMessages, ...recentMessages], "msg-29", { recentVerbatim: 12 });
const [summaryBlock] = transcript.split("\n\n"); const [summaryBlock] = transcript.split("\n\n");
const highlightLines = summaryBlock.split("\n").filter((line) => line.startsWith(" - ")); const highlightLines = summaryBlock.split("\n").filter((line) => line.startsWith(" - "));
@@ -89,8 +90,8 @@ describe("buildCompactedRoomTranscript", () => {
expect(summaryBlock).toContain("- Span: 18 messages"); expect(summaryBlock).toContain("- Span: 18 messages");
expect(summaryBlock).toContain("- Participants: User"); expect(summaryBlock).toContain("- Participants: User");
expect(summaryBlock).toContain("- Highlights:"); expect(summaryBlock).toContain("- Highlights:");
expect(summaryBlock.length).toBeLessThanOrEqual(1500); expect(summaryBlock.length).toBeLessThanOrEqual(3000);
expect(highlightLines.length).toBeLessThan(5); expect(highlightLines.length).toBeLessThanOrEqual(5);
}); });
it("keeps the total transcript under the overall cap", () => { it("keeps the total transcript under the overall cap", () => {
@@ -102,7 +103,7 @@ describe("buildCompactedRoomTranscript", () => {
const transcript = buildCompactedRoomTranscript(messages, "msg-79"); const transcript = buildCompactedRoomTranscript(messages, "msg-79");
expect(transcript.length).toBeLessThanOrEqual(8000); expect(transcript.length).toBeLessThanOrEqual(20000);
expect(transcript).toContain("message-79-"); expect(transcript).toContain("message-79-");
}); });
@@ -149,13 +150,13 @@ describe("buildCompactedRoomTranscript", () => {
})); }));
const settings = await (manager as any).getRoomCompactionSettings(); const settings = await (manager as any).getRoomCompactionSettings();
expect(settings).toEqual({ recentVerbatim: 12, fetchLimit: 80, summaryMaxChars: 1500 }); expect(settings).toEqual({ recentVerbatim: 25, fetchLimit: 200, summaryMaxChars: 3000 });
const managerWithThrow = new ChatManager({} as any, "/tmp", undefined, undefined, async () => { const managerWithThrow = new ChatManager({} as any, "/tmp", undefined, undefined, async () => {
throw new Error("boom"); throw new Error("boom");
}); });
const fallbackSettings = await (managerWithThrow as any).getRoomCompactionSettings(); const fallbackSettings = await (managerWithThrow as any).getRoomCompactionSettings();
expect(fallbackSettings).toEqual({ recentVerbatim: 12, fetchLimit: 80, summaryMaxChars: 1500 }); expect(fallbackSettings).toEqual({ recentVerbatim: 25, fetchLimit: 200, summaryMaxChars: 3000 });
}); });
it("computes unique participant labels from older messages", () => { it("computes unique participant labels from older messages", () => {
@@ -172,7 +173,7 @@ describe("buildCompactedRoomTranscript", () => {
content: `recent-${index}`, content: `recent-${index}`,
})); }));
const transcript = buildCompactedRoomTranscript([...older, ...recent], "msg-16"); const transcript = buildCompactedRoomTranscript([...older, ...recent], "msg-16", { recentVerbatim: 12 });
expect(transcript).toContain("- Participants: User, Agent agent-a, System, Assistant, Agent agent-b"); expect(transcript).toContain("- Participants: User, Agent agent-a, System, Assistant, Agent agent-b");
}); });

View File

@@ -252,7 +252,7 @@ describe("Chat orchestration — rooms (FN-3805..FN-3811 contract)", () => {
const prompt = promptSpy.mock.calls[0]?.[0] as string; const prompt = promptSpy.mock.calls[0]?.[0] as string;
expect(prompt).toContain("## Earlier room context (compacted)"); expect(prompt).toContain("## Earlier room context (compacted)");
expect(prompt).toContain("- Span: 18 messages from 2026-01-01T00:00:00.000Z to 2026-01-01T00:00:17.000Z"); expect(prompt).toContain("- Span: 5 messages from 2026-01-01T00:00:00.000Z to 2026-01-01T00:00:04.000Z");
expect(prompt).toContain("history-item-28"); expect(prompt).toContain("history-item-28");
expect(prompt).toContain(" - [2026-01-01T00:00:00.000Z] User: history-item-0"); expect(prompt).toContain(" - [2026-01-01T00:00:00.000Z] User: history-item-0");
expect(prompt).not.toContain("- [2026-01-01T00:00:00.000Z] (user) User: history-item-0"); expect(prompt).not.toContain("- [2026-01-01T00:00:00.000Z] (user) User: history-item-0");

View File

@@ -138,11 +138,11 @@ const MAX_MESSAGES_PER_IP_PER_MINUTE = 30;
/** Maximum file size for # mentions (50KB). Files larger than this are skipped. */ /** Maximum file size for # mentions (50KB). Files larger than this are skipped. */
const MAX_REFERENCED_FILE_SIZE = 50 * 1024; const MAX_REFERENCED_FILE_SIZE = 50 * 1024;
const ROOM_AMBIENT_MAX_RESPONDERS = 5; const ROOM_AMBIENT_MAX_RESPONDERS = 5;
const DEFAULT_ROOM_THREAD_RECENT_VERBATIM_MESSAGES = 12; const DEFAULT_ROOM_THREAD_RECENT_VERBATIM_MESSAGES = 25;
const DEFAULT_ROOM_THREAD_COMPACTION_FETCH_LIMIT = 80; const DEFAULT_ROOM_THREAD_COMPACTION_FETCH_LIMIT = 200;
const ROOM_THREAD_CONTEXT_MAX_CHARS = 8_000; const ROOM_THREAD_CONTEXT_MAX_CHARS = 20_000;
const ROOM_THREAD_MESSAGE_CONTENT_MAX_CHARS = 1_200; const ROOM_THREAD_MESSAGE_CONTENT_MAX_CHARS = 1_200;
const DEFAULT_ROOM_THREAD_SUMMARY_MAX_CHARS = 1_500; const DEFAULT_ROOM_THREAD_SUMMARY_MAX_CHARS = 3_000;
const IN_FLIGHT_PERSIST_DEBOUNCE_MS = 200; const IN_FLIGHT_PERSIST_DEBOUNCE_MS = 200;
type RoomTranscriptMessage = Pick<ChatRoomMessage, "id" | "role" | "content" | "createdAt" | "senderAgentId">; type RoomTranscriptMessage = Pick<ChatRoomMessage, "id" | "role" | "content" | "createdAt" | "senderAgentId">;

View File

@@ -119,7 +119,13 @@ describe("reliability interactions: same-agent duplicate intake", () => {
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-x" }, source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-x" },
}); });
vi.spyOn(fx.store, "listTasks").mockRejectedValueOnce(new Error("boom")); const originalListTasks = fx.store.listTasks.bind(fx.store);
vi.spyOn(fx.store, "listTasks").mockImplementation(async (options) => {
if (options?.slim === true && options?.includeArchived === false) {
throw new Error("boom");
}
return originalListTasks(options);
});
const b = await fx.store.createTask({ const b = await fx.store.createTask({
title: "fix: baseline clone", title: "fix: baseline clone",