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
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest'
const runLocalRemoteSessionMock = vi.hoisted(() => vi.fn(async (options: { session: { stopKeepAlive: () => void } }) => {
options.session.stopKeepAlive()
}))
vi.mock('@/agent/loopBase', () => ({
runLocalRemoteSession: runLocalRemoteSessionMock
}))
vi.mock('@/ui/logger', () => ({
logger: {
logFilePath: '/tmp/hapi.log',
debug: vi.fn()
}
}))
vi.mock('./claudeLocalLauncher', () => ({
claudeLocalLauncher: vi.fn()
}))
vi.mock('./claudeRemoteLauncher', () => ({
claudeRemoteLauncher: vi.fn()
}))
import { loop } from './loop'
describe('claude loop', () => {
it('initializes the Claude session id from resumeSessionId', async () => {
const sessionClient = {
keepAlive: vi.fn(),
emitMessagesConsumed: vi.fn(),
updateMetadata: vi.fn()
}
await loop({
path: '/tmp/project',
startingMode: 'local',
onModeChange: () => {},
mcpServers: {},
session: sessionClient as never,
api: {} as never,
messageQueue: {} as never,
hookSettingsPath: '/tmp/hooks.json',
resumeSessionId: '11111111-1111-4111-8111-111111111111'
})
expect(runLocalRemoteSessionMock).toHaveBeenCalledWith(expect.objectContaining({
session: expect.objectContaining({
sessionId: '11111111-1111-4111-8111-111111111111'
})
}))
})
})
+2 -1
View File
@@ -39,6 +39,7 @@ interface LoopOptions {
allowedTools?: string[]
onSessionReady?: (session: Session) => void
hookSettingsPath: string
resumeSessionId?: string
}
export async function loop(opts: LoopOptions) {
@@ -51,7 +52,7 @@ export async function loop(opts: LoopOptions) {
api: opts.api,
client: opts.session,
path: opts.path,
sessionId: null,
sessionId: opts.resumeSessionId ?? null,
claudeEnvVars: opts.claudeEnvVars,
claudeArgs: opts.claudeArgs,
mcpServers: opts.mcpServers,
+24 -10
View File
@@ -12,7 +12,8 @@ import { startHookServer } from '@/claude/utils/startHookServer';
import { generateHookSettingsFile, cleanupHookSettingsFile } from '@/modules/common/hooks/generateHookSettings';
import { registerKillSessionHandler } from './registerKillSessionHandler';
import type { Session } from './session';
import { bootstrapSession } from '@/agent/sessionFactory';
import { bootstrapExistingSession, bootstrapSession } from '@/agent/sessionFactory';
import { registerLocalHandoffHandler } from '@/agent/localHandoff';
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
import { PermissionModeSchema } from '@hapi/protocol/schemas';
@@ -30,10 +31,13 @@ export interface StartOptions {
claudeEnvVars?: Record<string, string>
claudeArgs?: string[]
startedBy?: 'runner' | 'terminal'
existingSessionId?: string
workingDirectory?: string
resumeSessionId?: string
}
export async function runClaude(options: StartOptions = {}): Promise<void> {
const workingDirectory = getInvokedCwd();
const workingDirectory = options.workingDirectory ?? getInvokedCwd();
const startedBy = options.startedBy ?? 'terminal';
// Log environment info at startup
@@ -51,14 +55,22 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const initialState: AgentState = {};
const initialModel = normalizeClaudeSessionModel(options.model);
const initialEffort = normalizeClaudeSessionEffort(options.effort);
const { api, session, sessionInfo } = await bootstrapSession({
flavor: 'claude',
startedBy,
workingDirectory,
agentState: initialState,
model: initialModel ?? undefined,
effort: initialEffort ?? undefined
});
const bootstrap = options.existingSessionId
? await bootstrapExistingSession({
sessionId: options.existingSessionId,
flavor: 'claude',
startedBy,
workingDirectory
})
: await bootstrapSession({
flavor: 'claude',
startedBy,
workingDirectory,
agentState: initialState,
model: initialModel ?? undefined,
effort: initialEffort ?? undefined
});
const { api, session, sessionInfo } = bootstrap;
logger.debug(`Session created: ${sessionInfo.id}`);
// Extract SDK metadata in background and update session when ready
@@ -133,6 +145,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
lifecycle.registerProcessHandlers();
registerKillSessionHandler(session.rpcHandlerManager, lifecycle.cleanupAndExit);
registerLocalHandoffHandler(session.rpcHandlerManager, lifecycle);
// Set initial agent state
const startingMode = options.startingMode ?? (startedBy === 'runner' ? 'remote' : 'local');
@@ -419,6 +432,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
claudeEnvVars: options.claudeEnvVars,
claudeArgs: options.claudeArgs,
startedBy,
resumeSessionId: options.resumeSessionId,
hookSettingsPath
});
} catch (error) {