mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
refactor(cli): unify session bootstrap
This commit is contained in:
@@ -1,26 +1,15 @@
|
||||
import os from 'node:os';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { ApiClient } from '@/api/api';
|
||||
import type { AgentState, Metadata } from '@/api/types';
|
||||
import type { AgentState } from '@/api/types';
|
||||
import { logger } from '@/ui/logger';
|
||||
import packageJson from '../../../package.json';
|
||||
import { readSettings } from '@/persistence';
|
||||
import { configuration } from '@/configuration';
|
||||
import { runtimePath } from '@/projectPath';
|
||||
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import { hashObject } from '@/utils/deterministicJson';
|
||||
import { AgentRegistry } from '@/agent/AgentRegistry';
|
||||
import { convertAgentMessage } from '@/agent/messageConverter';
|
||||
import { PermissionAdapter } from '@/agent/permissionAdapter';
|
||||
import type { AgentBackend, PromptContent } from '@/agent/types';
|
||||
import { notifyDaemonSessionStarted } from '@/daemon/controlClient';
|
||||
import { initialMachineMetadata } from '@/daemon/run';
|
||||
import { startHappyServer } from '@/claude/utils/startHappyServer';
|
||||
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
|
||||
import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler';
|
||||
import { readWorktreeEnv } from '@/utils/worktreeEnv';
|
||||
import { bootstrapSession } from '@/agent/sessionFactory';
|
||||
|
||||
function emitReadyIfIdle(props: {
|
||||
queueSize: () => number;
|
||||
@@ -38,56 +27,15 @@ export async function runAgentSession(opts: {
|
||||
agentType: string;
|
||||
startedBy?: 'daemon' | 'terminal';
|
||||
}): Promise<void> {
|
||||
const sessionTag = randomUUID();
|
||||
const api = await ApiClient.create();
|
||||
|
||||
const settings = await readSettings();
|
||||
const machineId = settings?.machineId;
|
||||
if (!machineId) {
|
||||
console.error(`[START] No machine ID found in settings. Please report this issue on ${packageJson.bugs}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await api.getOrCreateMachine({
|
||||
machineId,
|
||||
metadata: initialMachineMetadata
|
||||
});
|
||||
|
||||
let state: AgentState = {
|
||||
const initialState: AgentState = {
|
||||
controlledByUser: false
|
||||
};
|
||||
|
||||
const worktreeInfo = readWorktreeEnv();
|
||||
const metadata: Metadata = {
|
||||
path: process.cwd(),
|
||||
host: os.hostname(),
|
||||
version: packageJson.version,
|
||||
os: os.platform(),
|
||||
machineId,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir: runtimePath(),
|
||||
happyToolsDir: resolve(runtimePath(), 'tools', 'unpacked'),
|
||||
startedFromDaemon: opts.startedBy === 'daemon',
|
||||
hostPid: process.pid,
|
||||
startedBy: opts.startedBy || 'terminal',
|
||||
lifecycleState: 'running',
|
||||
lifecycleStateSince: Date.now(),
|
||||
const { session } = await bootstrapSession({
|
||||
flavor: opts.agentType,
|
||||
worktree: worktreeInfo ?? undefined
|
||||
};
|
||||
|
||||
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
||||
const session = api.sessionSyncClient(response);
|
||||
|
||||
try {
|
||||
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
||||
if (result.error) {
|
||||
logger.debug(`[START] Failed to report session to daemon: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('[START] Failed to report session to daemon', error);
|
||||
}
|
||||
startedBy: opts.startedBy ?? 'terminal',
|
||||
workingDirectory: process.cwd(),
|
||||
agentState: initialState
|
||||
});
|
||||
|
||||
session.updateAgentState((currentState) => ({
|
||||
...currentState,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import os from 'node:os'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { ApiClient } from '@/api/api'
|
||||
import type { ApiSessionClient } from '@/api/apiSession'
|
||||
import type { AgentState, MachineMetadata, Metadata, Session } from '@/api/types'
|
||||
import { notifyDaemonSessionStarted } from '@/daemon/controlClient'
|
||||
import { readSettings } from '@/persistence'
|
||||
import { configuration } from '@/configuration'
|
||||
import { logger } from '@/ui/logger'
|
||||
import { runtimePath } from '@/projectPath'
|
||||
import { readWorktreeEnv } from '@/utils/worktreeEnv'
|
||||
import packageJson from '../../package.json'
|
||||
|
||||
export type SessionStartedBy = 'daemon' | 'terminal'
|
||||
|
||||
export type SessionBootstrapOptions = {
|
||||
flavor: string
|
||||
startedBy?: SessionStartedBy
|
||||
workingDirectory?: string
|
||||
tag?: string
|
||||
agentState?: AgentState | null
|
||||
}
|
||||
|
||||
export type SessionBootstrapResult = {
|
||||
api: ApiClient
|
||||
session: ApiSessionClient
|
||||
sessionInfo: Session
|
||||
metadata: Metadata
|
||||
machineId: string
|
||||
startedBy: SessionStartedBy
|
||||
workingDirectory: string
|
||||
}
|
||||
|
||||
export function buildMachineMetadata(): MachineMetadata {
|
||||
return {
|
||||
host: os.hostname(),
|
||||
platform: os.platform(),
|
||||
happyCliVersion: packageJson.version,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir: runtimePath()
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSessionMetadata(options: {
|
||||
flavor: string
|
||||
startedBy: SessionStartedBy
|
||||
workingDirectory: string
|
||||
machineId: string
|
||||
now?: number
|
||||
}): Metadata {
|
||||
const happyLibDir = runtimePath()
|
||||
const worktreeInfo = readWorktreeEnv()
|
||||
const now = options.now ?? Date.now()
|
||||
|
||||
return {
|
||||
path: options.workingDirectory,
|
||||
host: os.hostname(),
|
||||
version: packageJson.version,
|
||||
os: os.platform(),
|
||||
machineId: options.machineId,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir,
|
||||
happyToolsDir: resolve(happyLibDir, 'tools', 'unpacked'),
|
||||
startedFromDaemon: options.startedBy === 'daemon',
|
||||
hostPid: process.pid,
|
||||
startedBy: options.startedBy,
|
||||
lifecycleState: 'running',
|
||||
lifecycleStateSince: now,
|
||||
flavor: options.flavor,
|
||||
worktree: worktreeInfo ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function getMachineIdOrExit(): Promise<string> {
|
||||
const settings = await readSettings()
|
||||
const machineId = settings?.machineId
|
||||
if (!machineId) {
|
||||
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on ${packageJson.bugs}`)
|
||||
process.exit(1)
|
||||
}
|
||||
logger.debug(`Using machineId: ${machineId}`)
|
||||
return machineId
|
||||
}
|
||||
|
||||
async function reportSessionStarted(sessionId: string, metadata: Metadata): Promise<void> {
|
||||
try {
|
||||
logger.debug(`[START] Reporting session ${sessionId} to daemon`)
|
||||
const result = await notifyDaemonSessionStarted(sessionId, metadata)
|
||||
if (result?.error) {
|
||||
logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error)
|
||||
} else {
|
||||
logger.debug(`[START] Reported session ${sessionId} to daemon`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('[START] Failed to report to daemon (may not be running):', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function bootstrapSession(options: SessionBootstrapOptions): Promise<SessionBootstrapResult> {
|
||||
const workingDirectory = options.workingDirectory ?? process.cwd()
|
||||
const startedBy = options.startedBy ?? 'terminal'
|
||||
const sessionTag = options.tag ?? randomUUID()
|
||||
const agentState = options.agentState === undefined ? {} : options.agentState
|
||||
|
||||
const api = await ApiClient.create()
|
||||
|
||||
const machineId = await getMachineIdOrExit()
|
||||
await api.getOrCreateMachine({
|
||||
machineId,
|
||||
metadata: buildMachineMetadata()
|
||||
})
|
||||
|
||||
const metadata = buildSessionMetadata({
|
||||
flavor: options.flavor,
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
machineId
|
||||
})
|
||||
|
||||
const sessionInfo = await api.getOrCreateSession({
|
||||
tag: sessionTag,
|
||||
metadata,
|
||||
state: agentState
|
||||
})
|
||||
|
||||
const session = api.sessionSyncClient(sessionInfo)
|
||||
|
||||
await reportSessionStarted(sessionInfo.id, metadata)
|
||||
|
||||
return {
|
||||
api,
|
||||
session,
|
||||
sessionInfo,
|
||||
metadata,
|
||||
machineId,
|
||||
startedBy,
|
||||
workingDirectory
|
||||
}
|
||||
}
|
||||
+11
-74
@@ -1,30 +1,19 @@
|
||||
import os from 'node:os';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { ApiClient } from '@/api/api';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { restoreTerminalState } from '@/ui/terminalState';
|
||||
import { loop } from '@/claude/loop';
|
||||
import { AgentState, Metadata, SessionModelMode } from '@/api/types';
|
||||
import packageJson from '../../package.json';
|
||||
import { readSettings } from '@/persistence';
|
||||
import { AgentState, SessionModelMode } from '@/api/types';
|
||||
import { EnhancedMode, PermissionMode } from './loop';
|
||||
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import { hashObject } from '@/utils/deterministicJson';
|
||||
import { extractSDKMetadataAsync } from '@/claude/sdk/metadataExtractor';
|
||||
import { parseSpecialCommand } from '@/parsers/specialCommands';
|
||||
import { getEnvironmentInfo } from '@/ui/doctor';
|
||||
import { configuration } from '@/configuration';
|
||||
import { notifyDaemonSessionStarted } from '@/daemon/controlClient';
|
||||
import { initialMachineMetadata } from '@/daemon/run';
|
||||
import { startHappyServer } from '@/claude/utils/startHappyServer';
|
||||
import { startHookServer } from '@/claude/utils/startHookServer';
|
||||
import { generateHookSettingsFile, cleanupHookSettingsFile } from '@/claude/utils/generateHookSettings';
|
||||
import { registerKillSessionHandler } from './registerKillSessionHandler';
|
||||
import { runtimePath } from '../projectPath';
|
||||
import { resolve } from 'node:path';
|
||||
import type { Session } from './session';
|
||||
import { readWorktreeEnv } from '@/utils/worktreeEnv';
|
||||
import { bootstrapSession } from '@/agent/sessionFactory';
|
||||
|
||||
export interface StartOptions {
|
||||
model?: string
|
||||
@@ -38,7 +27,6 @@ export interface StartOptions {
|
||||
|
||||
export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
const workingDirectory = process.cwd();
|
||||
const sessionTag = randomUUID();
|
||||
const startedBy = options.startedBy ?? 'terminal';
|
||||
|
||||
// Log environment info at startup
|
||||
@@ -53,69 +41,21 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
// throw new Error('Daemon-spawned sessions cannot use local/interactive mode');
|
||||
}
|
||||
|
||||
// Create session service
|
||||
const api = await ApiClient.create();
|
||||
|
||||
// Create a new session
|
||||
let state: AgentState = {};
|
||||
|
||||
// Get machine ID from settings (should already be set up)
|
||||
const settings = await readSettings();
|
||||
let machineId = settings?.machineId
|
||||
if (!machineId) {
|
||||
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on ${packageJson.bugs}`);
|
||||
process.exit(1);
|
||||
}
|
||||
logger.debug(`Using machineId: ${machineId}`);
|
||||
|
||||
// Create machine if it doesn't exist
|
||||
await api.getOrCreateMachine({
|
||||
machineId,
|
||||
metadata: initialMachineMetadata
|
||||
});
|
||||
|
||||
const worktreeInfo = readWorktreeEnv();
|
||||
let metadata: Metadata = {
|
||||
path: workingDirectory,
|
||||
host: os.hostname(),
|
||||
version: packageJson.version,
|
||||
os: os.platform(),
|
||||
machineId: machineId,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir: runtimePath(),
|
||||
happyToolsDir: resolve(runtimePath(), 'tools', 'unpacked'),
|
||||
startedFromDaemon: startedBy === 'daemon',
|
||||
hostPid: process.pid,
|
||||
startedBy,
|
||||
// Initialize lifecycle state
|
||||
lifecycleState: 'running',
|
||||
lifecycleStateSince: Date.now(),
|
||||
const initialState: AgentState = {};
|
||||
const { api, session, sessionInfo } = await bootstrapSession({
|
||||
flavor: 'claude',
|
||||
worktree: worktreeInfo ?? undefined
|
||||
};
|
||||
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
||||
logger.debug(`Session created: ${response.id}`);
|
||||
|
||||
// Always report to daemon if it exists
|
||||
try {
|
||||
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
||||
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
||||
if (result.error) {
|
||||
logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
||||
} else {
|
||||
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('[START] Failed to report to daemon (may not be running):', error);
|
||||
}
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
agentState: initialState
|
||||
});
|
||||
logger.debug(`Session created: ${sessionInfo.id}`);
|
||||
|
||||
// Extract SDK metadata in background and update session when ready
|
||||
extractSDKMetadataAsync(async (sdkMetadata) => {
|
||||
logger.debug('[start] SDK metadata extracted, updating session:', sdkMetadata);
|
||||
try {
|
||||
// Update session metadata with tools and slash commands
|
||||
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
||||
session.updateMetadata((currentMetadata) => ({
|
||||
...currentMetadata,
|
||||
tools: sdkMetadata.tools,
|
||||
slashCommands: sdkMetadata.slashCommands
|
||||
@@ -126,9 +66,6 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// Create realtime session
|
||||
const session = api.sessionSyncClient(response);
|
||||
|
||||
// Start HAPI MCP server
|
||||
const happyServer = await startHappyServer(session);
|
||||
logger.debug(`[START] HAPI MCP server started at ${happyServer.url}`);
|
||||
@@ -168,7 +105,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
|
||||
// Print log file path
|
||||
const logPath = logger.logFilePath;
|
||||
logger.infoDeveloper(`Session: ${response.id}`);
|
||||
logger.infoDeveloper(`Session: ${sessionInfo.id}`);
|
||||
logger.infoDeveloper(`Logs: ${logPath}`);
|
||||
|
||||
// Set initial agent state
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import os from 'node:os';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { ApiClient } from '@/api/api';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { restoreTerminalState } from '@/ui/terminalState';
|
||||
import { loop, type EnhancedMode, type PermissionMode } from './loop';
|
||||
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import { hashObject } from '@/utils/deterministicJson';
|
||||
import { readSettings } from '@/persistence';
|
||||
import { configuration } from '@/configuration';
|
||||
import { notifyDaemonSessionStarted } from '@/daemon/controlClient';
|
||||
import { initialMachineMetadata } from '@/daemon/run';
|
||||
import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler';
|
||||
import type { AgentState, Metadata } from '@/api/types';
|
||||
import packageJson from '../../package.json';
|
||||
import { runtimePath } from '@/projectPath';
|
||||
import type { AgentState } from '@/api/types';
|
||||
import type { CodexSession } from './session';
|
||||
import { parseCodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import { readWorktreeEnv } from '@/utils/worktreeEnv';
|
||||
import { bootstrapSession } from '@/agent/sessionFactory';
|
||||
|
||||
export { emitReadyIfIdle } from './utils/emitReadyIfIdle';
|
||||
|
||||
@@ -28,64 +17,19 @@ export async function runCodex(opts: {
|
||||
permissionMode?: PermissionMode;
|
||||
}): Promise<void> {
|
||||
const workingDirectory = process.cwd();
|
||||
const sessionTag = randomUUID();
|
||||
const startedBy = opts.startedBy ?? 'terminal';
|
||||
|
||||
logger.debug(`[codex] Starting with options: startedBy=${startedBy}`);
|
||||
|
||||
const api = await ApiClient.create();
|
||||
|
||||
const settings = await readSettings();
|
||||
const machineId = settings?.machineId;
|
||||
if (!machineId) {
|
||||
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on ${packageJson.bugs}`);
|
||||
process.exit(1);
|
||||
}
|
||||
logger.debug(`Using machineId: ${machineId}`);
|
||||
|
||||
await api.getOrCreateMachine({
|
||||
machineId,
|
||||
metadata: initialMachineMetadata
|
||||
});
|
||||
|
||||
let state: AgentState = {
|
||||
controlledByUser: false
|
||||
};
|
||||
|
||||
const worktreeInfo = readWorktreeEnv();
|
||||
const metadata: Metadata = {
|
||||
path: workingDirectory,
|
||||
host: os.hostname(),
|
||||
version: packageJson.version,
|
||||
os: os.platform(),
|
||||
machineId: machineId,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir: runtimePath(),
|
||||
happyToolsDir: resolve(runtimePath(), 'tools', 'unpacked'),
|
||||
startedFromDaemon: startedBy === 'daemon',
|
||||
hostPid: process.pid,
|
||||
startedBy,
|
||||
lifecycleState: 'running',
|
||||
lifecycleStateSince: Date.now(),
|
||||
const { api, session } = await bootstrapSession({
|
||||
flavor: 'codex',
|
||||
worktree: worktreeInfo ?? undefined
|
||||
};
|
||||
|
||||
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
||||
const session = api.sessionSyncClient(response);
|
||||
|
||||
try {
|
||||
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
||||
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
||||
if (result.error) {
|
||||
logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
||||
} else {
|
||||
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('[START] Failed to report to daemon (may not be running):', error);
|
||||
}
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
agentState: state
|
||||
});
|
||||
|
||||
const startingMode: 'local' | 'remote' = startedBy === 'daemon' ? 'remote' : 'local';
|
||||
|
||||
|
||||
+3
-14
@@ -3,11 +3,10 @@ import os from 'os';
|
||||
|
||||
import { ApiClient } from '@/api/api';
|
||||
import { TrackedSession } from './types';
|
||||
import { MachineMetadata, DaemonState, Metadata } from '@/api/types';
|
||||
import { DaemonState, Metadata } from '@/api/types';
|
||||
import { SpawnSessionOptions, SpawnSessionResult } from '@/modules/common/registerCommonHandlers';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { authAndSetupMachineIfNeeded } from '@/ui/auth';
|
||||
import { configuration } from '@/configuration';
|
||||
import packageJson from '../../package.json';
|
||||
import { getEnvironmentInfo } from '@/ui/doctor';
|
||||
import { spawnHappyCLI } from '@/utils/spawnHappyCLI';
|
||||
@@ -18,17 +17,7 @@ import { cleanupDaemonState, getInstalledCliMtimeMs, isDaemonRunningCurrentlyIns
|
||||
import { startDaemonControlServer } from './controlServer';
|
||||
import { createWorktree, removeWorktree, type WorktreeInfo } from './worktree';
|
||||
import { join } from 'path';
|
||||
import { runtimePath } from '@/projectPath';
|
||||
|
||||
// Prepare initial metadata
|
||||
export const initialMachineMetadata: MachineMetadata = {
|
||||
host: os.hostname(),
|
||||
platform: os.platform(),
|
||||
happyCliVersion: packageJson.version,
|
||||
homeDir: os.homedir(),
|
||||
happyHomeDir: configuration.happyHomeDir,
|
||||
happyLibDir: runtimePath()
|
||||
};
|
||||
import { buildMachineMetadata } from '@/agent/sessionFactory';
|
||||
|
||||
export async function startDaemon(): Promise<void> {
|
||||
// We don't have cleanup function at the time of server construction
|
||||
@@ -536,7 +525,7 @@ export async function startDaemon(): Promise<void> {
|
||||
// Get or create machine
|
||||
const machine = await api.getOrCreateMachine({
|
||||
machineId,
|
||||
metadata: initialMachineMetadata,
|
||||
metadata: buildMachineMetadata(),
|
||||
daemonState: initialDaemonState
|
||||
});
|
||||
logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
||||
|
||||
Reference in New Issue
Block a user