nexus/.planning/phases/36-voice-pipeline-foundation/36-02-PLAN.md

11 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
36-voice-pipeline-foundation 02 execute 1
packages/shared/src/validators/chat.ts
packages/shared/src/types/chat.ts
server/src/services/nexus-settings.ts
server/src/__tests__/36-voice-schema.test.ts
true
VPIPE-05
truths artifacts key_links
createMessageSchema accepts voiceMode field with values text, voice_input, or full_voice
createMessageSchema strips unknown fields but preserves voiceMode when present
ChatMessage interface includes optional voiceMode field
nexus-settings schema accepts voiceMode and telegramToken fields
Existing nexus-settings.json files without voiceMode parse without error (defaults to text)
path provides contains
packages/shared/src/validators/chat.ts voiceMode field on createMessageSchema voiceMode
path provides contains
packages/shared/src/types/chat.ts voiceMode on ChatMessage interface voiceMode
path provides contains
server/src/services/nexus-settings.ts voiceMode and telegramToken in settings schema voiceMode
path provides min_lines
server/src/__tests__/36-voice-schema.test.ts Schema validation tests for voiceMode 40
from to via pattern
packages/shared/src/validators/chat.ts server/src/routes/chat.ts createMessageSchema.parse(req.body) preserves voiceMode createMessageSchema
from to via pattern
server/src/services/nexus-settings.ts nexus-settings.json Zod .default() handles missing voiceMode in existing files voiceMode.*default.*"text"
Extend shared type definitions and settings schema with voiceMode support so the voice flag can propagate through the entire message pipeline.

Purpose: VPIPE-05 requires the voiceMode flag to survive from client request through Express route to message persistence. This plan adds the schema and type foundations that Plan 03 wires together.

Output: Updated createMessageSchema, ChatMessage interface, and nexus-settings schema with voiceMode and telegramToken fields, plus tests.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/36-voice-pipeline-foundation/36-RESEARCH.md

From packages/shared/src/validators/chat.ts:

export const createMessageSchema = z.object({
  role: z.enum(["user", "assistant", "system"]),
  content: z.string().min(1).max(100_000),
  agentId: z.string().uuid().optional(),
  messageType: z.string().optional(),
});
export type CreateMessage = z.infer<typeof createMessageSchema>;

From packages/shared/src/types/chat.ts:

export interface ChatMessage {
  id: string;
  conversationId: string;
  role: "user" | "assistant" | "system";
  content: string;
  agentId: string | null;
  messageType: string | null;
  createdAt: string;
  updatedAt: string | null;
  files?: ChatFile[];
}

From server/src/services/nexus-settings.ts:

export const NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const;
const nexusSettingsSchema = z.object({
  mode: z.enum(NEXUS_MODES).default("both"),
  voiceEnabled: z.boolean().default(false),
});
Task 1: Extend shared validators and types with voiceMode field packages/shared/src/validators/chat.ts packages/shared/src/types/chat.ts server/src/__tests__/36-voice-schema.test.ts packages/shared/src/validators/chat.ts packages/shared/src/types/chat.ts - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "full_voice" }) succeeds and output.voiceMode equals "full_voice" - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "voice_input" }) succeeds - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "text" }) succeeds - createMessageSchema.parse({ role: "user", content: "hi" }) succeeds and output.voiceMode is undefined - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "invalid" }) throws ZodError - Existing createMessageSchema behavior unchanged (role, content, agentId, messageType all work as before) 1. Create `server/src/__tests__/36-voice-schema.test.ts` (RED): - Import `createMessageSchema` from `@paperclipai/shared/validators/chat` - Test: parse with voiceMode "full_voice" returns voiceMode "full_voice" - Test: parse with voiceMode "voice_input" returns voiceMode "voice_input" - Test: parse with voiceMode "text" returns voiceMode "text" - Test: parse without voiceMode returns undefined for voiceMode - Test: parse with voiceMode "invalid" throws - Test: existing fields (role, content, agentId, messageType) still parse correctly
2. Modify `packages/shared/src/validators/chat.ts` (GREEN):
   - Add to createMessageSchema object: `voiceMode: z.enum(["text", "voice_input", "full_voice"]).optional(),`
   - Add after existing exports:
     ```typescript
     export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const;
     export type VoiceMode = (typeof VOICE_MODES)[number];
     ```

3. Modify `packages/shared/src/types/chat.ts` (GREEN):
   - Add to ChatMessage interface after `messageType: string | null;`:
     ```typescript
     voiceMode?: "text" | "voice_input" | "full_voice" | null;
     ```
cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts - packages/shared/src/validators/chat.ts contains `voiceMode: z.enum(["text", "voice_input", "full_voice"]).optional()` - packages/shared/src/validators/chat.ts contains `export const VOICE_MODES` - packages/shared/src/validators/chat.ts contains `export type VoiceMode` - packages/shared/src/types/chat.ts contains `voiceMode?:` in ChatMessage interface - server/src/__tests__/36-voice-schema.test.ts exits 0 createMessageSchema accepts optional voiceMode field with enum validation. ChatMessage interface includes voiceMode. VOICE_MODES constant and VoiceMode type are exported for reuse. Task 2: Extend nexus-settings schema with voiceMode and telegramToken server/src/services/nexus-settings.ts server/src/__tests__/36-voice-schema.test.ts server/src/services/nexus-settings.ts server/src/__tests__/36-voice-schema.test.ts - nexusSettingsSchema parses { mode: "both" } and output.voiceMode equals "text" (default) - nexusSettingsSchema parses { mode: "both", voiceMode: "full_voice" } and output.voiceMode equals "full_voice" - nexusSettingsSchema parses { mode: "both", telegramToken: "123:ABC" } and output.telegramToken equals "123:ABC" - nexusSettingsSchema parses { mode: "both" } and output.telegramToken is undefined - Existing settings without voiceMode/telegramToken parse without error 1. Add tests to `server/src/__tests__/36-voice-schema.test.ts` (RED): - Import `nexusSettingsService` from `../services/nexus-settings.js` - Note: since nexusSettingsService reads from disk, test the schema directly instead. Import the schema or test via the service's `get()` method with a mocked file system. - Alternative approach: directly import and test the Zod schema. If the schema is not exported, add a named export `nexusSettingsSchema` for testing, or test through the service. - Test: settings without voiceMode defaults to "text" - Test: settings with voiceMode "full_voice" preserves the value - Test: settings with telegramToken preserves the value - Test: settings without telegramToken has telegramToken as undefined
2. Modify `server/src/services/nexus-settings.ts` (GREEN):
   - Add after existing NEXUS_MODES:
     ```typescript
     export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const;
     export type VoiceMode = (typeof VOICE_MODES)[number];
     ```
   - Extend nexusSettingsSchema:
     ```typescript
     const nexusSettingsSchema = z.object({
       mode: z.enum(NEXUS_MODES).default("both"),
       voiceEnabled: z.boolean().default(false),
       voiceMode: z.enum(VOICE_MODES).default("text"),
       telegramToken: z.string().optional(),
       piperBinaryPath: z.string().optional(),
       whisperBinaryPath: z.string().optional(),
     });
     ```
   - Export the schema for testing: `export { nexusSettingsSchema };`
   - The `NexusSettings` type (already `z.infer<typeof nexusSettingsSchema>`) auto-updates
   - Update the fallback return in `get()` catch block from `{ mode: "both", voiceEnabled: false }` to `nexusSettingsSchema.parse({})` so it uses Zod defaults consistently
cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts - server/src/services/nexus-settings.ts contains `voiceMode: z.enum(VOICE_MODES).default("text")` - server/src/services/nexus-settings.ts contains `telegramToken: z.string().optional()` - server/src/services/nexus-settings.ts contains `piperBinaryPath: z.string().optional()` - server/src/services/nexus-settings.ts contains `whisperBinaryPath: z.string().optional()` - server/src/services/nexus-settings.ts contains `export const VOICE_MODES` - server/src/__tests__/36-voice-schema.test.ts exits 0 nexus-settings schema accepts voiceMode (defaulting to "text"), telegramToken, piperBinaryPath, and whisperBinaryPath. Existing nexus-settings.json files without these fields parse without error due to Zod defaults. - `pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts` exits 0 - `grep "voiceMode" packages/shared/src/validators/chat.ts` shows the field - `grep "voiceMode" packages/shared/src/types/chat.ts` shows the field - `grep "voiceMode" server/src/services/nexus-settings.ts` shows the field - `grep "telegramToken" server/src/services/nexus-settings.ts` shows the field

<success_criteria> The voiceMode field is accepted by createMessageSchema (shared package), present on the ChatMessage type, and configurable in nexus-settings with a safe "text" default. telegramToken is ready for Phase 38. All schema tests pass. </success_criteria>

After completion, create `.planning/phases/36-voice-pipeline-foundation/36-02-SUMMARY.md`