diff --git a/docs/architecture.md b/docs/architecture.md index e06c44a2a..c2979a1e1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 9248341f9..fc731c505 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -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 (1–64 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: diff --git a/packages/core/src/__tests__/global-settings.test.ts b/packages/core/src/__tests__/global-settings.test.ts index 20d43a011..6cab98606 100644 --- a/packages/core/src/__tests__/global-settings.test.ts +++ b/packages/core/src/__tests__/global-settings.test.ts @@ -326,6 +326,7 @@ describe("GlobalSettingsStore", () => { "planning-awaiting-input", "gridlock", "fallback-used", + "memory-dreams-processed", ]); }); diff --git a/packages/core/src/__tests__/notification-dispatcher.test.ts b/packages/core/src/__tests__/notification-dispatcher.test.ts index 02df75e03..db68168f0 100644 --- a/packages/core/src/__tests__/notification-dispatcher.test.ts +++ b/packages/core/src/__tests__/notification-dispatcher.test.ts @@ -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([]); }); diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 44959b5f6..ad37e27cb 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -32,6 +32,7 @@ export const DEFAULT_GLOBAL_SETTINGS = { "planning-awaiting-input", "gridlock", "fallback-used", + "memory-dreams-processed", ], ntfyDashboardHost: undefined, webhookEnabled: false, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a4be60cd3..a557f88aa 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -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. */ diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 6c4992a5a..b708f022d 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -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. diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx index cebe77ba2..ba1f01d0e 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx @@ -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); }); }); diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx index 1c5ef3d8f..bae8e6dbc 100644 --- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx @@ -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,