Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions docs/AUDIO_TRIGGER_POINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Audio Notification Trigger Points

This document identifies where audio notifications should be triggered in the haflow workflow system.

## Identified Trigger Points

### 1. Mission Moves to Waiting Human State
**Location**: `packages/backend/src/services/mission-engine.ts:62-63`
**Event**: Mission status changes to `waiting_human` after an agent step completes
**Method**: `advanceToNextStep()` when next step is type `human-gate`
**Data Available**:
- `missionId`: The mission ID
- `meta.current_step`: Current step index
- Next step type and name

**Notification Details**:
- Priority: `high` (human review required)
- Action: Notify humans that a mission requires review

### 2. Mission Moves to Waiting Code Review State
**Location**: `packages/backend/src/services/mission-engine.ts:64-65`
**Event**: Mission status changes to `waiting_code_review` after agent step completes
**Method**: `advanceToNextStep()` when next step is type `code-review`
**Data Available**:
- `missionId`: The mission ID
- `meta.current_step`: Current step index
- Next step type and name

**Notification Details**:
- Priority: `standard` (code review needed)
- Action: Notify humans that a mission has code changes requiring review

### 3. Mission Created
**Location**: `packages/backend/src/services/mission-store.ts:36-78`
**Event**: New mission is created
**Method**: `createMission()` determines initial status
**Data Available**:
- `missionId`: The mission ID
- `title`: Mission title
- `type`: Mission type (feature, fix, bugfix, hotfix, enhance)
- `first_step`: First step in workflow

**Notification Details**:
- Priority: `low` (informational)
- Action: Notify that a new mission has been created

## Notification Handler Integration Points

### Potential Integration Locations

1. **In `advanceToNextStep()` after status change**
- Emit event when entering `waiting_human` or `waiting_code_review` states
- Include step information and mission details

2. **In `startAgentStep()` completion**
- Emit event when agent step completes
- Include completion details

3. **Custom Event Emitter Pattern**
- Create an event emitter that publishes workflow events
- Audio notification system subscribes to specific events

## Workflow Status Transitions

```
draft
[human-gate] → waiting_human (TRIGGER: Audio notification for high priority)
[agent] → ready
[human-gate] → waiting_human (TRIGGER: Audio notification for high priority)
[agent] → ready
[code-review] → waiting_code_review (TRIGGER: Audio notification for standard priority)
completed
```

## Implementation Strategy

### Option 1: Direct Event Emission
Emit audio notification events directly in `mission-engine.ts` when status changes occur.

**Pros**:
- Simple, direct implementation
- Clear trigger point visibility

**Cons**:
- Couples business logic with notification logic
- Hard to test independently

### Option 2: Event Bus Pattern
Create an event bus that publishes workflow events to subscribers.

**Pros**:
- Decoupled architecture
- Easy to test and extend
- Can support multiple subscribers

**Cons**:
- Additional abstraction layer
- Requires event bus implementation

### Option 3: Webhook Pattern
When status changes, make HTTP calls to registered webhook endpoints.

**Pros**:
- Works for distributed systems
- External services can subscribe

**Cons**:
- Network overhead
- Requires webhook endpoint in frontend

## Recommended Approach

**Implement Event Bus Pattern** with the following structure:

1. Create `src/services/event-bus.ts` in backend
2. Emit `mission:waiting_human` and `mission:waiting_code_review` events
3. Backend maintains a simple in-memory listener for audio notifications
4. Frontend can subscribe via WebSocket or polling for real-time notifications

## Example Event Structure

```typescript
interface WorkflowEvent {
type: 'mission:waiting_human' | 'mission:waiting_code_review' | 'mission:created';
missionId: string;
timestamp: number;
data: {
title: string;
stepName: string;
stepIndex: number;
priority: 'high' | 'standard' | 'low';
};
}
```

## Frontend Integration

The frontend audio notification system should:

1. Connect to the backend API polling endpoint
2. When new notification event arrives, check user preferences
3. If audio enabled for priority level, play the appropriate sound
4. Display visual notification

## Implementation Checklist

- [ ] Event structure defined
- [ ] Event emission points identified and documented
- [ ] Audio notification handler created
- [ ] Frontend polling or WebSocket implementation
- [ ] User preference integration
- [ ] Error handling and fallbacks
- [ ] Tests for event emission and handling
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { createServer } from './server.js';
import { config } from './utils/config.js';
import { missionStore } from './services/mission-store.js';
import { missionEngine } from './services/mission-engine.js';
import { userPreferencesService } from './services/user-preferences.js';

async function main() {
// Initialize stores and engine
await missionStore.init();
await missionEngine.init();
await userPreferencesService.init();

const app = createServer();

Expand Down
66 changes: 66 additions & 0 deletions packages/backend/src/routes/user-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Router, type Router as RouterType, type Request, type Response } from 'express';
import { userPreferencesService } from '../services/user-preferences.js';
import { sendSuccess, sendError } from '../utils/response.js';
import type { AudioNotificationPreferences } from '@haflow/shared';

export const userPreferencesRoutes: RouterType = Router();

// GET /api/user/preferences - Fetch current user preferences
userPreferencesRoutes.get('/preferences', async (req: Request, res: Response, next) => {
try {
// Get user ID from session/auth - for now use a default
// In a real app, this would come from req.user or similar
const userId = (req as any).userId || 'default-user';

const preferences = await userPreferencesService.getUserPreferences(userId);
sendSuccess(res, preferences);
} catch (error) {
next(error);
}
});

// PUT /api/user/preferences - Update user preferences
userPreferencesRoutes.put('/preferences', async (req: Request, res: Response, next) => {
try {
const userId = (req as any).userId || 'default-user';
const preferences = req.body as AudioNotificationPreferences;

await userPreferencesService.updateUserPreferences(userId, preferences);

// Return updated preferences
const updated = await userPreferencesService.getUserPreferences(userId);
sendSuccess(res, updated);
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('validation')) {
return sendError(res, `Invalid preferences: ${error.message}`, 400);
}
return sendError(res, error.message, 500);
}
next(error);
}
});

// POST /api/user/preferences/reset - Reset to default preferences
userPreferencesRoutes.post('/preferences/reset', async (req: Request, res: Response, next) => {
try {
const userId = (req as any).userId || 'default-user';

await userPreferencesService.resetToDefaults(userId);

const defaults = await userPreferencesService.getUserPreferences(userId);
sendSuccess(res, defaults);
} catch (error) {
next(error);
}
});

// GET /api/user/preferences/defaults - Get default preferences
userPreferencesRoutes.get('/preferences/defaults', async (req: Request, res: Response) => {
try {
const defaults = userPreferencesService.getDefaultPreferences();
sendSuccess(res, defaults);
} catch (error) {
sendError(res, 'Failed to get default preferences', 500);
}
});
2 changes: 2 additions & 0 deletions packages/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import cors from 'cors';
import { missionRoutes, workflowRoutes } from './routes/missions.js';
import { transcriptionRoutes } from './routes/transcription.js';
import { systemRoutes } from './routes/system.js';
import { userPreferencesRoutes } from './routes/user-preferences.js';

export function createServer(): Express {
const app = express();
Expand All @@ -17,6 +18,7 @@ export function createServer(): Express {
app.use('/api/workflows', workflowRoutes);
app.use('/api/transcribe', transcriptionRoutes);
app.use('/api/system', systemRoutes);
app.use('/api/user', userPreferencesRoutes);

// Error handler
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
Expand Down
91 changes: 91 additions & 0 deletions packages/backend/src/services/user-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { mkdir, readFile, writeFile } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
import type { AudioNotificationPreferences } from '@haflow/shared';
import { AudioNotificationPreferencesSchema } from '@haflow/shared';
import { config } from '../utils/config.js';

const preferencesDir = () => {
const dir = join(config.haflowHome, 'user-preferences');
return dir;
};

const preferenceFile = (userId: string) => join(preferencesDir(), `${userId}.json`);

// Default preferences for new users
const getDefaultPreferences = (): AudioNotificationPreferences => ({
audioNotifications: {
enabled: false,
volume: 50,
profiles: {
highPriority: { sound: 'alert-urgent.wav', enabled: true },
standardPriority: { sound: 'alert-standard.wav', enabled: true },
lowPriority: { sound: 'alert-low.wav', enabled: false },
},
},
visualNotifications: {
enabled: true,
},
});

async function init(): Promise<void> {
const dir = preferencesDir();
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
}

async function getUserPreferences(userId: string): Promise<AudioNotificationPreferences> {
try {
const path = preferenceFile(userId);

if (!existsSync(path)) {
// Return default preferences for new users
return getDefaultPreferences();
}

const content = await readFile(path, 'utf-8');
const parsed = JSON.parse(content);

// Validate against schema
return AudioNotificationPreferencesSchema.parse(parsed);
} catch (error) {
console.error(`Failed to load preferences for user ${userId}:`, error);
return getDefaultPreferences();
}
}

async function updateUserPreferences(
userId: string,
preferences: AudioNotificationPreferences
): Promise<void> {
try {
// Validate schema
const validated = AudioNotificationPreferencesSchema.parse(preferences);

const path = preferenceFile(userId);
const dir = preferencesDir();

if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}

// Write preferences file
await writeFile(path, JSON.stringify(validated, null, 2));
} catch (error) {
throw new Error(`Failed to update preferences for user ${userId}: ${error}`);
}
}

async function resetToDefaults(userId: string): Promise<void> {
const defaults = getDefaultPreferences();
await updateUserPreferences(userId, defaults);
}

export const userPreferencesService = {
init,
getUserPreferences,
updateUserPreferences,
resetToDefaults,
getDefaultPreferences,
};
Loading
Loading