feat(FN-3180): add dreams notification event and harden plugin setup sync

This merge lands four features: a new "dreams" notification event exposed through settings and docs (FN-3180), hardened plugin setup sync with test isolation improvements (FN-3573), and feedback-driven enhancements to the native shell connection manager (FN-3402). Core settings schema, the plugin AP

Fusion-Task-Id: FN-3180
This commit is contained in:
Fusion
2026-05-06 14:36:50 -07:00
committed by gsxdsm
parent ea485b28bb
commit f22db4d1fe
9 changed files with 20 additions and 8 deletions

View File

@@ -450,6 +450,7 @@ See [Memory Plugin Contract](./memory-plugin-contract.md) for the full plan.
- Compatibility scope: `NtfyNotifier` remains responsible for gridlock-only compatibility notifications (`notifyGridlock`) and legacy helper APIs.
- Legacy gridlock ntfy delivery is cooldown-throttled: first detection notifies immediately, subsequent detections are suppressed for 15 minutes (even if blocked-task membership changes), and the cooldown resets as soon as gridlock fully clears.
- `NotificationService` (`notification/notification-service.ts`) — provider lifecycle + event dispatch orchestration
- Subscribes to task lifecycle events plus non-task memory events. Manual `POST /api/memory/dream` processing emits `store.emit("memory:dreams-processed", payload)` when new DREAMS content is written, and `NotificationService` dispatches that as the `memory-dreams-processed` notification event to ntfy/webhook providers (event-filter controlled, no task deep link required).
- `NotificationProvider` interface (`@fusion/core` `notification/provider.ts`) — pluggable provider contract
- Built-in providers: `NtfyNotificationProvider` (`notification/ntfy-provider.ts`), `WebhookNotificationProvider` (`notification/webhook-provider.ts`)
- `AgentReflection` (`agent-reflection.ts`) — reflection extraction and persistence

View File

@@ -42,7 +42,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
| `ntfyEnabled` | `boolean` | `false` | Enable ntfy push notifications. |
| `ntfyTopic` | `string` | `undefined` | ntfy topic name. |
| `ntfyBaseUrl` | `string` | `undefined` | Optional custom ntfy server base URL (must use `http://` or `https://`). If blank/unset, Fusion uses `https://ntfy.sh` for both runtime and test notifications. |
| `ntfyEvents` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "fallback-used")[]` | `["in-review","merged","failed","awaiting-approval","awaiting-user-review","planning-awaiting-input","gridlock","fallback-used"]` | Event types that trigger ntfy notifications. `planning-awaiting-input` fires when planning mode is waiting on user input. `gridlock` fires when all schedulable todo tasks are blocked; delivery is cooldown-throttled (first alert immediately, then suppressed for 15 minutes until gridlock resolves). `fallback-used` fires when Fusion recovers from a retryable model failure by switching to a configured fallback model. |
| `ntfyEvents` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "fallback-used" \| "memory-dreams-processed")[]` | `["in-review","merged","failed","awaiting-approval","awaiting-user-review","planning-awaiting-input","gridlock","fallback-used","memory-dreams-processed"]` | Event types that trigger ntfy notifications. `planning-awaiting-input` fires when planning mode is waiting on user input. `gridlock` fires when all schedulable todo tasks are blocked; delivery is cooldown-throttled (first alert immediately, then suppressed for 15 minutes until gridlock resolves). `fallback-used` fires when Fusion recovers from a retryable model failure by switching to a configured fallback model. `memory-dreams-processed` fires when manual dream processing writes a new `DREAMS.md` entry (project and/or agent); disable it via ntfy/webhook event filters if you want to opt out. |
| `ntfyDashboardHost` | `string` | `undefined` | Dashboard host used to build deep links in notifications. |
| `webhookEnabled` | `boolean` | `false` | Enable webhook notifications for task lifecycle events. Part of the legacy flat settings; prefer `notificationProviders` for new setups. |
| `webhookUrl` | `string` | `undefined` | Webhook endpoint URL. Must be `http://` or `https://`. Part of legacy flat settings. |
@@ -130,7 +130,7 @@ When `id` is `"webhook"`, the provider `config` supports:
|---|---|---:|---|
| `webhookUrl` | `string` | _required_ | Must be a valid `http://` or `https://` URL. |
| `webhookFormat` | `"slack" \| "discord" \| "generic"` | `"generic"` | Invalid/omitted values fall back to `"generic"`. |
| `events` | `string[]` | `[]` | Event filter list. Empty/omitted means all events are sent. |
| `events` | `string[]` | `[]` | Event filter list. Empty/omitted means all events are sent. Includes `memory-dreams-processed` for DREAMS.md updates from manual dream processing. |
#### ntfy provider config
@@ -140,7 +140,7 @@ When `id` is `"ntfy"` in `notificationProviders`, the provider `config` supports
|---|---|---:|---|
| `topic` | `string` | _required_ | ntfy topic name (164 chars, alphanumeric + `-_`). |
| `ntfyBaseUrl` | `string` | `"https://ntfy.sh"` | Optional custom ntfy server URL. |
| `events` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock")[]` | `DEFAULT_NTFY_EVENTS` | Event filter list used by the provider. For `gridlock`, enabled events are still cooldown-throttled at runtime (15-minute suppression window, reset on full resolution). |
| `events` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "fallback-used" \| "memory-dreams-processed")[]` | `DEFAULT_NTFY_EVENTS` | Event filter list used by the provider. For `gridlock`, enabled events are still cooldown-throttled at runtime (15-minute suppression window, reset on full resolution). `memory-dreams-processed` is emitted when manual dream processing appends a new project/agent `DREAMS.md` entry. |
| `dashboardHost` | `string` | `undefined` | Dashboard host for deep links in notifications. |
Disable daily update checks globally:

View File

@@ -326,6 +326,7 @@ describe("GlobalSettingsStore", () => {
"planning-awaiting-input",
"gridlock",
"fallback-used",
"memory-dreams-processed",
]);
});

View File

@@ -138,8 +138,8 @@ describe("NotificationDispatcher", () => {
});
it("keeps ntfy defaults unchanged and adds notificationProviders default", () => {
const event: NtfyNotificationEvent = "in-review";
expect(event).toBe("in-review");
const event: NtfyNotificationEvent = "memory-dreams-processed";
expect(event).toBe("memory-dreams-processed");
expect(DEFAULT_GLOBAL_SETTINGS.ntfyEnabled).toBe(false);
expect(DEFAULT_GLOBAL_SETTINGS.ntfyTopic).toBeUndefined();
@@ -154,6 +154,7 @@ describe("NotificationDispatcher", () => {
"planning-awaiting-input",
"gridlock",
"fallback-used",
"memory-dreams-processed",
]);
expect(DEFAULT_GLOBAL_SETTINGS.notificationProviders).toEqual([]);
});

View File

@@ -32,6 +32,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
"planning-awaiting-input",
"gridlock",
"fallback-used",
"memory-dreams-processed",
],
ntfyDashboardHost: undefined,
webhookEnabled: false,

View File

@@ -226,7 +226,8 @@ export type NtfyNotificationEvent =
| "awaiting-user-review"
| "planning-awaiting-input"
| "gridlock"
| "fallback-used";
| "fallback-used"
| "memory-dreams-processed";
/** Known notification event types. Providers may support additional custom events. */
export const NOTIFICATION_EVENTS = [
@@ -238,6 +239,7 @@ export const NOTIFICATION_EVENTS = [
"planning-awaiting-input",
"gridlock",
"fallback-used",
"memory-dreams-processed",
] as const;
/** Notification event type. Known events plus provider-specific custom events. */

View File

@@ -243,6 +243,7 @@ const DEFAULT_NTFY_EVENTS: NtfyNotificationEvent[] = [
"planning-awaiting-input",
"gridlock",
"fallback-used",
"memory-dreams-processed",
];
const NOTIFICATION_EVENT_OPTIONS: Array<{ event: NtfyNotificationEvent; label: string; description: string }> = [
@@ -254,6 +255,7 @@ const NOTIFICATION_EVENT_OPTIONS: Array<{ event: NtfyNotificationEvent; label: s
{ event: "planning-awaiting-input", label: "Planning needs input", description: "When planning mode is waiting for your response to continue" },
{ event: "gridlock", label: "Pipeline gridlocked", description: "When all schedulable todo tasks are blocked and work cannot advance" },
{ event: "fallback-used", label: "Fallback model used (recovered)", description: "When Fusion recovers from a retryable model failure by switching to a fallback model" },
{ event: "memory-dreams-processed", label: "DREAMS.md entry added", description: "When manual dream processing writes a new entry to project or agent DREAMS.md" },
];
/** Well-known experimental feature flags with display labels.

View File

@@ -2578,16 +2578,18 @@ describe("SettingsModal", () => {
expect(screen.getByRole("button", { name: /Test notification/ })).toBeInTheDocument();
});
it("shows fallback-used event option for both providers", async () => {
it("shows fallback-used and dreams events for both providers", async () => {
mockFetchSettings.mockResolvedValueOnce({ ...defaultSettings, ntfyEnabled: true, ntfyTopic: "test-topic" });
renderModal();
await waitForSettingsModalReady();
await openNotificationsSection();
expect(screen.getByLabelText("Fallback model used (recovered)")).toBeInTheDocument();
expect(screen.getByLabelText("DREAMS.md entry added")).toBeInTheDocument();
await userEvent.click(screen.getByLabelText("Webhook notifications"));
expect(screen.getAllByLabelText("Fallback model used (recovered)").length).toBeGreaterThan(0);
expect(screen.getAllByLabelText("DREAMS.md entry added").length).toBeGreaterThan(0);
});
it("shows webhook fields when webhook provider is enabled", async () => {
@@ -2668,10 +2670,12 @@ describe("SettingsModal", () => {
const failed = screen.getByLabelText("Task failed") as HTMLInputElement;
const merged = screen.getByLabelText("Task merged") as HTMLInputElement;
const fallbackUsed = screen.getByLabelText("Fallback model used (recovered)") as HTMLInputElement;
const dreamsProcessed = screen.getByLabelText("DREAMS.md entry added") as HTMLInputElement;
expect(inReview.checked).toBe(true);
expect(failed.checked).toBe(true);
expect(merged.checked).toBe(false);
expect(fallbackUsed.checked).toBe(false);
expect(dreamsProcessed.checked).toBe(false);
});
});

View File

@@ -28,7 +28,7 @@ const defaultSettings = {
defaultPresetBySize: {},
ntfyEnabled: false,
ntfyTopic: undefined,
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "fallback-used"],
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "fallback-used", "memory-dreams-processed"],
webhookEnabled: false,
webhookUrl: undefined,
webhookFormat: undefined,