refactor: share machine runner schemas

This commit is contained in:
weishu
2026-05-21 14:29:42 +08:00
parent 7d5a5ee919
commit 9704227ce2
8 changed files with 48 additions and 126 deletions
+1
View File
@@ -194,6 +194,7 @@ export class ApiClient {
return {
id: raw.id,
namespace: raw.namespace,
seq: raw.seq,
createdAt: raw.createdAt,
updatedAt: raw.updatedAt,
+1
View File
@@ -24,6 +24,7 @@ import type { Machine } from './types'
function makeMachine(id: string): Machine {
return {
id,
namespace: 'default',
seq: 1,
createdAt: 0,
updatedAt: 0,
+1 -3
View File
@@ -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)}`)
+9 -72
View File
@@ -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<typeof MachineMetadataSchema>
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<typeof RunnerStateSchema>
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<typeof CreateSessionResponseSchema>
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<typeof CreateMachineResponseSchema>
+11 -34
View File
@@ -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<string, Machine> = new Map()
private readonly lastBroadcastAtByMachineId: Map<string, number> = 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
}
+21 -1
View File
@@ -236,6 +236,26 @@ export const MachineMetadataSchema = z.object({
workspaceRoots: z.array(z.string()).optional()
})
export type MachineMetadata = z.infer<typeof MachineMetadataSchema>
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<typeof RunnerStateSchema>
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()
})
+2
View File
@@ -6,8 +6,10 @@ export type {
DecryptedMessage,
Metadata,
Machine,
MachineMetadata,
MachinePatch,
MachineUpdatedData,
RunnerState,
Session,
SessionPatch,
SessionUpdatedData,
+2 -16
View File
@@ -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: {