From 9704227ce2915f43eb8621f9e9733b253eae1e48 Mon Sep 17 00:00:00 2001 From: weishu Date: Thu, 21 May 2026 14:29:42 +0800 Subject: [PATCH] refactor: share machine runner schemas --- cli/src/api/api.ts | 1 + cli/src/api/apiMachine.test.ts | 1 + cli/src/api/apiMachine.ts | 4 +- cli/src/api/types.ts | 81 ++++------------------------------ hub/src/sync/machineCache.ts | 45 +++++-------------- shared/src/schemas.ts | 22 ++++++++- shared/src/types.ts | 2 + web/src/types/api.ts | 18 +------- 8 files changed, 48 insertions(+), 126 deletions(-) diff --git a/cli/src/api/api.ts b/cli/src/api/api.ts index af5d6981..b8c80bbd 100644 --- a/cli/src/api/api.ts +++ b/cli/src/api/api.ts @@ -194,6 +194,7 @@ export class ApiClient { return { id: raw.id, + namespace: raw.namespace, seq: raw.seq, createdAt: raw.createdAt, updatedAt: raw.updatedAt, diff --git a/cli/src/api/apiMachine.test.ts b/cli/src/api/apiMachine.test.ts index 784ea7d4..95603941 100644 --- a/cli/src/api/apiMachine.test.ts +++ b/cli/src/api/apiMachine.test.ts @@ -24,6 +24,7 @@ import type { Machine } from './types' function makeMachine(id: string): Machine { return { id, + namespace: 'default', seq: 1, createdAt: 0, updatedAt: 0, diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 4cb3fe13..c517d9b9 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -451,9 +451,7 @@ export class ApiMachineClient { if (desiredWorkspaceRoots?.length) { return { ...base, workspaceRoots: desiredWorkspaceRoots } } - const { workspaceRoot: _legacyWorkspaceRoot, workspaceRoots: _workspaceRoots, ...rest } = base as MachineMetadata & { - workspaceRoot?: string - } + const { workspaceRoots: _workspaceRoots, ...rest } = base return rest as MachineMetadata }).then(() => { console.log(`[HAPI] Workspace roots synced: ${formatWorkspaceRoots(this.machine.metadata?.workspaceRoots)}`) diff --git a/cli/src/api/types.ts b/cli/src/api/types.ts index 03186f79..da35643f 100644 --- a/cli/src/api/types.ts +++ b/cli/src/api/types.ts @@ -2,8 +2,11 @@ import { AgentStateSchema, AttachmentMetadataSchema, CodexCollaborationModeSchema, + MachineMetadataSchema, + MachineSchema, MetadataSchema, PermissionModeSchema, + RunnerStateSchema, TodosSchema } from '@hapi/protocol/schemas' import { @@ -11,7 +14,7 @@ import { LocalResumeTargetResponseSchema, ResumableSessionsResponseSchema } from '@hapi/protocol' -import type { CodexCollaborationMode, PermissionMode } from '@hapi/protocol/types' +import type { CodexCollaborationMode, Machine, MachineMetadata, PermissionMode, RunnerState } from '@hapi/protocol/types' import { z } from 'zod' import { UsageSchema } from '@/claude/types' @@ -23,7 +26,10 @@ export type { ClaudePermissionMode, CodexCollaborationMode, CodexPermissionMode, + Machine, + MachineMetadata, Metadata, + RunnerState, Session } from '@hapi/protocol/types' export type SessionPermissionMode = PermissionMode @@ -32,65 +38,7 @@ export type SessionModel = string | null export type SessionModelReasoningEffort = string | null export type SessionEffort = string | null -export { AgentStateSchema, AttachmentMetadataSchema, MetadataSchema } - -export const MachineMetadataSchema = z.object({ - host: z.string(), - platform: z.string(), - happyCliVersion: z.string(), - displayName: z.string().optional(), - homeDir: z.string(), - happyHomeDir: z.string(), - happyLibDir: z.string(), - workspaceRoot: z.string().optional(), - workspaceRoots: z.array(z.string()).optional() -}).transform(({ workspaceRoot, workspaceRoots, ...rest }) => { - const normalizedWorkspaceRoots = Array.from(new Set( - Array.isArray(workspaceRoots) - ? workspaceRoots.filter((path): path is string => typeof path === 'string' && path.trim().length > 0) - : workspaceRoot - ? [workspaceRoot] - : [] - )) - - return { - ...rest, - workspaceRoots: normalizedWorkspaceRoots.length > 0 ? normalizedWorkspaceRoots : undefined - } -}) - -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 type Machine = { - id: string - seq: number - createdAt: number - updatedAt: number - active: boolean - activeAt: number - metadata: MachineMetadata | null - metadataVersion: number - runnerState: RunnerState | null - runnerStateVersion: number -} +export { AgentStateSchema, AttachmentMetadataSchema, MachineMetadataSchema, MetadataSchema, RunnerStateSchema } export const CliMessagesResponseSchema = z.object({ messages: z.array(z.object({ @@ -131,18 +79,7 @@ export const CreateSessionResponseSchema = z.object({ export type CreateSessionResponse = z.infer export const CreateMachineResponseSchema = z.object({ - machine: z.object({ - id: z.string(), - seq: z.number(), - createdAt: z.number(), - updatedAt: z.number(), - active: z.boolean(), - activeAt: z.number(), - metadata: z.unknown().nullable(), - metadataVersion: z.number(), - runnerState: z.unknown().nullable(), - runnerStateVersion: z.number() - }) + machine: MachineSchema }) export type CreateMachineResponse = z.infer diff --git a/hub/src/sync/machineCache.ts b/hub/src/sync/machineCache.ts index da5720b9..750db3e5 100644 --- a/hub/src/sync/machineCache.ts +++ b/hub/src/sync/machineCache.ts @@ -1,21 +1,9 @@ -import { z } from 'zod' import type { Machine, MachinePatch } from '@hapi/protocol/types' +import { MachineMetadataSchema, RunnerStateSchema } from '@hapi/protocol/schemas' import type { Store } from '../store' import { clampAliveTime } from './aliveTime' import { EventPublisher } from './eventPublisher' -const machineMetadataSchema = z.object({ - host: z.string().optional(), - platform: z.string().optional(), - happyCliVersion: z.string().optional(), - displayName: z.string().optional(), - homeDir: z.string().optional(), - happyHomeDir: z.string().optional(), - happyLibDir: z.string().optional(), - workspaceRoot: z.string().optional(), - workspaceRoots: z.array(z.string()).optional() -}) - export class MachineCache { private readonly machines: Map = new Map() private readonly lastBroadcastAtByMachineId: Map = new Map() @@ -72,35 +60,24 @@ export class MachineCache { const existing = this.machines.get(machineId) const metadata = (() => { - const parsed = machineMetadataSchema.safeParse(stored.metadata) + const parsed = MachineMetadataSchema.safeParse(stored.metadata) if (!parsed.success) return null const data = parsed.data - const host = typeof data.host === 'string' ? data.host : 'unknown' - const platform = typeof data.platform === 'string' ? data.platform : 'unknown' - const happyCliVersion = typeof data.happyCliVersion === 'string' ? data.happyCliVersion : 'unknown' - const displayName = typeof data.displayName === 'string' ? data.displayName : undefined - const homeDir = typeof data.homeDir === 'string' ? data.homeDir : undefined - const happyHomeDir = typeof data.happyHomeDir === 'string' ? data.happyHomeDir : undefined - const happyLibDir = typeof data.happyLibDir === 'string' ? data.happyLibDir : undefined const workspaceRoots = Array.from(new Set( - Array.isArray(data.workspaceRoots) - ? data.workspaceRoots.filter((path): path is string => typeof path === 'string' && path.trim().length > 0) - : typeof data.workspaceRoot === 'string' - ? [data.workspaceRoot] - : [] + (data.workspaceRoots ?? []).filter((path) => path.trim().length > 0) )) return { - host, - platform, - happyCliVersion, - displayName, - homeDir, - happyHomeDir, - happyLibDir, + ...data, workspaceRoots: workspaceRoots.length > 0 ? workspaceRoots : undefined } })() + const runnerState = (() => { + if (stored.runnerState == null) return null + const parsed = RunnerStateSchema.safeParse(stored.runnerState) + return parsed.success ? parsed.data : null + })() + const storedActiveAt = stored.activeAt ?? stored.createdAt const existingActiveAt = existing?.activeAt ?? 0 const useStoredActivity = storedActiveAt > existingActiveAt @@ -115,7 +92,7 @@ export class MachineCache { activeAt: useStoredActivity ? storedActiveAt : (existingActiveAt || storedActiveAt), metadata, metadataVersion: stored.metadataVersion, - runnerState: stored.runnerState, + runnerState, runnerStateVersion: stored.runnerStateVersion } diff --git a/shared/src/schemas.ts b/shared/src/schemas.ts index ae071554..d9c3dfdd 100644 --- a/shared/src/schemas.ts +++ b/shared/src/schemas.ts @@ -236,6 +236,26 @@ export const MachineMetadataSchema = z.object({ 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 MachineSchema = z.object({ id: z.string(), namespace: z.string(), @@ -246,7 +266,7 @@ export const MachineSchema = z.object({ activeAt: z.number(), metadata: MachineMetadataSchema.nullable(), metadataVersion: z.number(), - runnerState: z.unknown().nullable(), + runnerState: RunnerStateSchema.nullable(), runnerStateVersion: z.number() }) diff --git a/shared/src/types.ts b/shared/src/types.ts index 3d04adbe..d195af19 100644 --- a/shared/src/types.ts +++ b/shared/src/types.ts @@ -6,8 +6,10 @@ export type { DecryptedMessage, Metadata, Machine, + MachineMetadata, MachinePatch, MachineUpdatedData, + RunnerState, Session, SessionPatch, SessionUpdatedData, diff --git a/web/src/types/api.ts b/web/src/types/api.ts index a7aaf10e..0d940f9d 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -1,6 +1,7 @@ import type { DecryptedMessage as ProtocolDecryptedMessage, Machine, + RunnerState, Session, SessionSummary, SyncEvent as ProtocolSyncEvent, @@ -33,6 +34,7 @@ export type { CodexCollaborationMode, PermissionMode, Machine, + RunnerState, Session, SessionPatch, SessionSummary, @@ -71,22 +73,6 @@ export type DecryptedMessage = ProtocolDecryptedMessage & { invokedAt?: number | null } -export type RunnerState = { - status?: string - pid?: number - httpPort?: number - startedAt?: number - shutdownRequestedAt?: number - shutdownSource?: string - lastSpawnError?: { - message: string - pid?: number - exitCode?: number | null - signal?: string | null - at: number - } | null -} - export type AuthResponse = { token: string user: {