import { z } from 'zod' import { CODEX_COLLABORATION_MODES, PERMISSION_MODES } from './modes' export const PermissionModeSchema = z.enum(PERMISSION_MODES) export const CodexCollaborationModeSchema = z.enum(CODEX_COLLABORATION_MODES) export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error', 'handoff']) export type SessionEndReason = z.infer const MetadataSummarySchema = z.object({ text: z.string(), updatedAt: z.number() }) const SessionCapabilitiesSchema = z.object({ terminal: z.boolean().optional() }) export const WorktreeMetadataSchema = z.object({ basePath: z.string(), branch: z.string(), name: z.string(), worktreePath: z.string().optional(), createdAt: z.number().optional() }) export type WorktreeMetadata = z.infer export const MetadataSchema = z.object({ path: z.string(), host: z.string(), version: z.string().optional(), name: z.string().optional(), os: z.string().optional(), summary: MetadataSummarySchema.optional(), machineId: z.string().optional(), claudeSessionId: z.string().optional(), codexSessionId: z.string().optional(), // 原始 Codex thread id。导入 Codex 历史后,HAPI 会 fork 出自己的续写 thread; // codexSessionId 保存 fork 后的 thread,codexSourceSessionId 保留来源 thread 便于同步/展示。 codexSourceSessionId: z.string().optional(), geminiSessionId: z.string().optional(), opencodeSessionId: z.string().optional(), grokSessionId: z.string().optional(), cursorSessionId: z.string().optional(), cursorSessionProtocol: z.enum(['acp', 'stream-json']).optional(), // Drives the web `CursorMigrationBanner`: // 'in_progress' = legacy-to-ACP transplant currently running; banner shows spinner + "Upgrading..." // 'ambiguous' = migrator refused to transplant (ambiguous source drawer OR size mismatch); // banner switches to "Manual review needed" until the operator resolves on disk. // undefined = no migration in flight; banner hidden. // tiann/hapi#873. cursorMigrationState: z.enum(['in_progress', 'ambiguous']).optional(), kimiSessionId: z.string().optional(), piSessionId: z.string().optional(), tools: z.array(z.string()).optional(), slashCommands: z.array(z.string()).optional(), homeDir: z.string().optional(), happyHomeDir: z.string().optional(), happyLibDir: z.string().optional(), happyToolsDir: z.string().optional(), startedFromRunner: z.boolean().optional(), hostPid: z.number().optional(), hapiMcpUrl: z.string().url().optional(), startedBy: z.enum(['runner', 'terminal']).optional(), lifecycleState: z.string().optional(), lifecycleStateSince: z.number().optional(), archivedBy: z.string().optional(), archiveReason: z.string().optional(), preferredPermissionMode: PermissionModeSchema.optional(), flavor: z.string().nullish(), capabilities: SessionCapabilitiesSchema.optional(), worktree: WorktreeMetadataSchema.optional(), // Cached Pi model list — written by CLI, read by web (inactive session fallback). // Minimal shape: each entry must have modelId; other fields (provider, name, etc.) pass through. piAvailableModels: z.array(z.object({ modelId: z.string() }).passthrough()).optional(), // Pi-selected model with provider identity. The legacy `session.model` // field stores only modelId (shared across all flavors); this preserves // the provider so web can resolve the exact model when two providers // share a modelId. piSelectedModel: z.object({ provider: z.string(), modelId: z.string() }).nullable().optional() }) export type Metadata = z.infer export const AgentStateRequestSchema = z.object({ tool: z.string(), arguments: z.unknown(), createdAt: z.number().nullish() }) export type AgentStateRequest = z.infer export const AgentStateCompletedRequestSchema = z.object({ tool: z.string(), arguments: z.unknown(), createdAt: z.number().nullish(), completedAt: z.number().nullish(), status: z.enum(['canceled', 'denied', 'approved']), reason: z.string().optional(), mode: z.string().optional(), decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).optional(), allowTools: z.array(z.string()).optional(), // Flat format: Record (AskUserQuestion) // Nested format: Record (request_user_input) answers: z.union([ z.record(z.string(), z.array(z.string())), z.record(z.string(), z.object({ answers: z.array(z.string()) })) ]).optional() }) export type AgentStateCompletedRequest = z.infer export const AgentStateSchema = z.object({ controlledByUser: z.boolean().nullish(), requests: z.record(z.string(), AgentStateRequestSchema).nullish(), completedRequests: z.record(z.string(), AgentStateCompletedRequestSchema).nullish() }) export type AgentState = z.infer export const TodoItemSchema = z.object({ content: z.string(), status: z.enum(['pending', 'in_progress', 'completed']), priority: z.enum(['high', 'medium', 'low']).optional().default('medium'), id: z.string().optional().default(''), activeForm: z.string().optional() }) export type TodoItem = z.infer export const TodosSchema = z.array(TodoItemSchema) export const TeamMemberSchema = z.object({ name: z.string(), agentType: z.string().optional(), status: z.enum(['active', 'idle', 'shutdown']).optional() }) export type TeamMember = z.infer export const TeamTaskSchema = z.object({ id: z.string(), title: z.string(), description: z.string().optional(), status: z.enum(['pending', 'in_progress', 'completed', 'blocked']).optional(), owner: z.string().optional() }) export type TeamTask = z.infer export const TeamMessageSchema = z.object({ from: z.string(), to: z.string(), summary: z.string(), type: z.enum(['message', 'broadcast', 'shutdown_request', 'shutdown_response']), timestamp: z.number() }) export type TeamMessage = z.infer export const TeamStateSchema = z.object({ teamName: z.string(), description: z.string().optional(), members: z.array(TeamMemberSchema).optional(), tasks: z.array(TeamTaskSchema).optional(), messages: z.array(TeamMessageSchema).optional(), updatedAt: z.number().optional() }) export type TeamState = z.infer export const ThreadGoalStatusSchema = z.enum(['active', 'paused', 'budgetLimited', 'complete']) export type ThreadGoalStatus = z.infer export const ThreadGoalSchema = z.object({ threadId: z.string(), objective: z.string(), status: ThreadGoalStatusSchema, tokenBudget: z.number().nullable().optional(), tokensUsed: z.number().optional().default(0), timeUsedSeconds: z.number().optional().default(0), createdAt: z.number().optional().default(0), updatedAt: z.number().optional().default(0) }) export type ThreadGoal = z.infer export const AttachmentMetadataSchema = z.object({ id: z.string(), filename: z.string(), mimeType: z.string(), size: z.number(), path: z.string(), previewUrl: z.string().optional() }) export type AttachmentMetadata = z.infer export const DecryptedMessageSchema = z.object({ id: z.string(), seq: z.number().nullable(), localId: z.string().nullable(), content: z.unknown(), createdAt: z.number(), invokedAt: z.number().nullable().optional(), scheduledAt: z.number().nullable().optional() }) export type DecryptedMessage = z.infer export const SessionSchema = z.object({ id: z.string(), namespace: z.string(), seq: z.number(), createdAt: z.number(), updatedAt: z.number(), active: z.boolean(), // Hub may still emit null for legacy SQLite rows; keep output type number. activeAt: z.number().nullish().transform((value) => value ?? 0), metadata: MetadataSchema.nullable(), metadataVersion: z.number(), agentState: AgentStateSchema.nullable(), agentStateVersion: z.number(), thinking: z.boolean(), thinkingAt: z.number(), backgroundTaskCount: z.number().optional(), todos: TodosSchema.optional(), teamState: TeamStateSchema.optional(), model: z.string().nullable().optional().default(null), modelReasoningEffort: z.string().nullable().optional().default(null), effort: z.string().nullable().optional().default(null), serviceTier: z.string().nullable().optional().default(null), permissionMode: PermissionModeSchema.optional(), collaborationMode: CodexCollaborationModeSchema.optional() }) export type Session = z.infer export const SessionPatchSchema = z.object({ active: z.boolean().optional(), thinking: z.boolean().optional(), activeAt: z.number().optional(), updatedAt: z.number().optional(), model: z.string().nullable().optional(), modelReasoningEffort: z.string().nullable().optional(), effort: z.string().nullable().optional(), serviceTier: z.string().nullable().optional(), permissionMode: PermissionModeSchema.optional(), collaborationMode: CodexCollaborationModeSchema.optional(), backgroundTaskCount: z.number().optional() }).strict() export type SessionPatch = z.infer export const MachineMetadataSchema = z.object({ host: z.string(), platform: z.string(), happyCliVersion: z.string(), displayName: z.string().optional(), homeDir: z.string().optional(), happyHomeDir: z.string().optional(), happyLibDir: z.string().optional(), workspaceRoots: z.array(z.string()).optional() }) export type MachineMetadata = z.infer export const RunnerStateSchema = z.object({ status: z.union([z.enum(['running', 'shutting-down']), z.string()]), pid: z.number().optional(), httpPort: z.number().optional(), startedAt: z.number().optional(), shutdownRequestedAt: z.number().optional(), shutdownSource: z.union([z.enum(['mobile-app', 'cli', 'os-signal', 'unknown']), z.string()]).optional(), lastSpawnError: z.object({ message: z.string(), pid: z.number().optional(), exitCode: z.number().nullable().optional(), signal: z.string().nullable().optional(), at: z.number() }).nullable().optional() }) export type RunnerState = z.infer export const MachineHealthSchema = z.object({ collectedAt: z.number(), cpuCount: z.number().int().positive().optional(), load1m: z.number().nonnegative().optional(), cpuPercent: z.number().min(0).max(100).optional(), memoryPercent: z.number().min(0).max(100).optional(), uptimeSeconds: z.number().nonnegative().optional() }).strict() export type MachineHealth = z.infer export const MachineSchema = z.object({ id: z.string(), namespace: z.string(), seq: z.number(), createdAt: z.number(), updatedAt: z.number(), active: z.boolean(), activeAt: z.number(), metadata: MachineMetadataSchema.nullable(), metadataVersion: z.number(), runnerState: RunnerStateSchema.nullable(), runnerStateVersion: z.number(), health: MachineHealthSchema.nullable().optional() }) export type Machine = z.infer export const MachinePatchSchema = z.object({ active: z.boolean().optional(), activeAt: z.number().optional(), updatedAt: z.number().optional() }).strict() export type MachinePatch = z.infer export const SessionUpdatedDataSchema = z.union([SessionSchema, SessionPatchSchema]) export type SessionUpdatedData = z.infer export const MachineUpdatedDataSchema = z.union([MachineSchema, MachinePatchSchema, z.null()]) export type MachineUpdatedData = z.infer const SessionEventBaseSchema = z.object({ namespace: z.string().optional() }) const SessionChangedSchema = SessionEventBaseSchema.extend({ sessionId: z.string() }) const MachineChangedSchema = SessionEventBaseSchema.extend({ machineId: z.string() }) export const SyncEventSchema = z.discriminatedUnion('type', [ SessionChangedSchema.extend({ type: z.literal('session-added'), data: z.unknown().optional() }), SessionChangedSchema.extend({ type: z.literal('session-updated'), data: SessionUpdatedDataSchema.optional() }), SessionEventBaseSchema.extend({ type: z.literal('session-removed'), sessionId: z.string() }), SessionChangedSchema.extend({ type: z.literal('message-received'), message: DecryptedMessageSchema }), SessionChangedSchema.extend({ type: z.literal('messages-invalidated') }), SessionChangedSchema.extend({ type: z.literal('scheduled-matured') }), SessionChangedSchema.extend({ type: z.literal('session-ended'), reason: SessionEndReasonSchema.optional() }), MachineChangedSchema.extend({ type: z.literal('machine-updated'), data: MachineUpdatedDataSchema.optional() }), SessionEventBaseSchema.extend({ type: z.literal('toast'), data: z.object({ title: z.string(), body: z.string(), sessionId: z.string(), url: z.string() }) }), SessionChangedSchema.extend({ type: z.literal('messages-consumed'), localIds: z.array(z.string()), invokedAt: z.number() }), SessionChangedSchema.extend({ type: z.literal('message-cancelled'), messageId: z.string(), localId: z.string().optional() }), SessionEventBaseSchema.extend({ type: z.literal('heartbeat'), data: z.object({ timestamp: z.number() }).optional() }), SessionEventBaseSchema.extend({ type: z.literal('connection-changed'), data: z.object({ status: z.string(), subscriptionId: z.string().optional() }).optional() }) ]) export type SyncEvent = z.infer export const CancelMessageResponseSchema = z.discriminatedUnion('status', [ z.object({ status: z.literal('cancelled'), localId: z.string().nullable() }), z.object({ status: z.literal('invoked'), message: DecryptedMessageSchema }), ]) export type CancelMessageResponse = z.infer