feat: add hapi resume command (#647)

This commit is contained in:
leko
2026-05-20 06:18:42 +08:00
committed by GitHub
parent 79d919675e
commit 197f327590
29 changed files with 1777 additions and 93 deletions
+1
View File
@@ -2,6 +2,7 @@ export * from './messages'
export * from './flavors'
export * from './models'
export * from './modes'
export * from './resume'
export * from './socket'
export * from './sessionSummary'
export * from './utils'
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'bun:test'
import { LocalResumeTargetSchema, ResumableSessionSchema } from './resume'
import { SyncEventSchema } from './schemas'
import { SessionEndReasonSchema } from './socket'
describe('resume schemas', () => {
it('accepts a local resume target', () => {
const parsed = LocalResumeTargetSchema.safeParse({
sessionId: 'hapi-session-1',
flavor: 'codex',
directory: '/tmp/project',
machineId: 'machine-1',
host: 'devbox',
active: true,
thinking: false,
controlledByUser: false,
agentSessionId: 'codex-thread-1',
model: 'gpt-5.4',
effort: null,
modelReasoningEffort: 'xhigh',
permissionMode: 'default',
collaborationMode: 'default'
})
expect(parsed.success).toBe(true)
})
it('accepts a resumable session summary', () => {
const parsed = ResumableSessionSchema.safeParse({
sessionId: 'hapi-session-1',
flavor: 'claude',
directory: '/tmp/project',
active: false,
thinking: false,
controlledByUser: false,
agentSessionId: '11111111-1111-4111-8111-111111111111',
updatedAt: 123,
name: 'project work',
summary: 'finish docs'
})
expect(parsed.success).toBe(true)
})
it('accepts handoff as a session end reason', () => {
expect(SessionEndReasonSchema.parse('handoff')).toBe('handoff')
})
it('accepts handoff in session-ended sync events', () => {
const parsed = SyncEventSchema.safeParse({
type: 'session-ended',
sessionId: 'hapi-session-1',
reason: 'handoff'
})
expect(parsed.success).toBe(true)
})
})
+49
View File
@@ -0,0 +1,49 @@
import { z } from 'zod'
import { CodexCollaborationModeSchema, PermissionModeSchema } from './schemas'
export const AgentFlavorSchema = z.enum(['claude', 'codex', 'gemini', 'opencode', 'cursor'])
export const LocalResumeTargetSchema = z.object({
sessionId: z.string().min(1),
flavor: AgentFlavorSchema,
directory: z.string().min(1),
machineId: z.string().optional(),
host: z.string().optional(),
active: z.boolean(),
thinking: z.boolean(),
controlledByUser: z.boolean(),
agentSessionId: z.string().min(1),
model: z.string().nullable().optional(),
effort: z.string().nullable().optional(),
modelReasoningEffort: z.string().nullable().optional(),
permissionMode: PermissionModeSchema.optional(),
collaborationMode: CodexCollaborationModeSchema.optional()
})
export type LocalResumeTarget = z.infer<typeof LocalResumeTargetSchema>
export const ResumableSessionSchema = LocalResumeTargetSchema.extend({
updatedAt: z.number(),
name: z.string().optional(),
summary: z.string().optional()
})
export type ResumableSession = z.infer<typeof ResumableSessionSchema>
export const LocalResumeTargetResponseSchema = z.object({
target: LocalResumeTargetSchema
})
export type LocalResumeTargetResponse = z.infer<typeof LocalResumeTargetResponseSchema>
export const ResumableSessionsResponseSchema = z.object({
sessions: z.array(ResumableSessionSchema)
})
export type ResumableSessionsResponse = z.infer<typeof ResumableSessionsResponseSchema>
export const LocalHandoffResponseSchema = z.object({
ok: z.boolean()
})
export type LocalHandoffResponse = z.infer<typeof LocalHandoffResponseSchema>
+3 -1
View File
@@ -3,6 +3,8 @@ 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<typeof SessionEndReasonSchema>
const MetadataSummarySchema = z.object({
text: z.string(),
@@ -242,7 +244,7 @@ export const SyncEventSchema = z.discriminatedUnion('type', [
}),
SessionChangedSchema.extend({
type: z.literal('session-ended'),
reason: z.enum(['completed', 'terminated', 'error']).optional()
reason: SessionEndReasonSchema.optional()
}),
MachineChangedSchema.extend({
type: z.literal('machine-updated'),
+2 -3
View File
@@ -1,5 +1,7 @@
import { z } from 'zod'
import type { CodexCollaborationMode, PermissionMode } from './modes'
import type { SessionEndReason } from './schemas'
export { SessionEndReasonSchema, type SessionEndReason } from './schemas'
export type SocketErrorReason = 'namespace-missing' | 'access-denied' | 'not-found'
@@ -67,9 +69,6 @@ export const TerminalErrorPayloadSchema = z.object({
})
export type TerminalErrorPayload = z.infer<typeof TerminalErrorPayloadSchema>
export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error'])
export type SessionEndReason = z.infer<typeof SessionEndReasonSchema>
export const UpdateNewMessageBodySchema = z.object({
t: z.literal('new-message'),
sid: z.string(),