refactor(cli): unify session bootstrap

This commit is contained in:
weishu
2026-01-03 15:34:49 +08:00
parent 8ce568ea70
commit 89b6fc34f7
5 changed files with 172 additions and 211 deletions
+8 -60
View File
@@ -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,
+143
View File
@@ -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
}
}