Compare commits

...

6 Commits

Author SHA1 Message Date
claude-code-best
2a5b263641 chore: 1.9.4 2026-04-24 10:50:53 +08:00
claude-code-best
f2dd5142b3 refactor: 解耦 BRIDGE_MODE 与 DAEMON,禁用 DAEMON 降低内存占用
- 从 DEFAULT_BUILD_FEATURES 注释掉 DAEMON(内存占用过高)
- remoteControlServer 命令门控从 feature('DAEMON') && feature('BRIDGE_MODE')
  改为仅 feature('BRIDGE_MODE'),bridge 不再依赖 daemon
- --daemon-worker 快速路径改为运行时检测,未启用时输出明确错误提示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:01:05 +08:00
claude-code-best
4dcbaf1e66 fix: 修复 ACP 模式下 messageSelector require 失败导致 submitMessage 崩溃
ACP 模式不加载完整的 React/Ink UI 组件,导致 require('src/components/MessageSelector.js')
返回 undefined。添加 try-catch 和 optional chaining fallback。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:59:23 +08:00
claude-code-best
0b304730d8 docs: 为 DEFAULT_BUILD_FEATURES 每个 feature flag 添加功能注释
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:26:59 +08:00
claude-code-best
7a0dd3057e chore: 1.9.3 2026-04-23 23:21:43 +08:00
claude-code-best
ca1c87f460 fix: 修复 usePipeIpc 中 require 返回 undefined 导致启动崩溃
将 lazy require() 调用全部替换为静态 import,解决构建产物中
模块加载时序问题导致的 'undefined is not an object' 错误。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 23:21:38 +08:00
7 changed files with 130 additions and 127 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-code-best", "name": "claude-code-best",
"version": "1.9.2", "version": "1.9.4",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal", "description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"type": "module", "type": "module",
"author": "claude-code-best <claude-code-best@proton.me>", "author": "claude-code-best <claude-code-best@proton.me>",

View File

@@ -27,51 +27,49 @@ export function getMacroDefines(): Record<string, string> {
* - scripts/dev.ts (bun run dev) * - scripts/dev.ts (bun run dev)
*/ */
export const DEFAULT_BUILD_FEATURES = [ export const DEFAULT_BUILD_FEATURES = [
'BUDDY', 'TRANSCRIPT_CLASSIFIER', 'BRIDGE_MODE', 'BUDDY', // 陪伴宠物角色Squirtle Waddles
'AGENT_TRIGGERS_REMOTE', 'TRANSCRIPT_CLASSIFIER', // 对话分类器,用于标注会话类型
'CHICAGO_MCP', 'BRIDGE_MODE', // Remote Control / Bridge 模式,远程控制会话
'VOICE_MODE', 'AGENT_TRIGGERS_REMOTE', // Agent 触发远程会话连接
'SHOT_STATS', 'CHICAGO_MCP', // Chicago MCP 集成(内部代号)
'PROMPT_CACHE_BREAK_DETECTION', 'VOICE_MODE', // Push-to-Talk 语音输入模式
'TOKEN_BUDGET', 'SHOT_STATS', // 单次请求统计信息收集
'PROMPT_CACHE_BREAK_DETECTION', // 检测 prompt cache 是否被打破
'TOKEN_BUDGET', // Token 预算管理与控制
// P0: local features // P0: local features
'AGENT_TRIGGERS', 'AGENT_TRIGGERS', // 本地 Agent 触发器(工具调用时启动子代理)
'ULTRATHINK', 'ULTRATHINK', // 超深度思考模式,增加推理链长度
'BUILTIN_EXPLORE_PLAN_AGENTS', 'BUILTIN_EXPLORE_PLAN_AGENTS', // 内置 Explore/Plan 子代理类型
'LODESTONE', 'LODESTONE', // 上下文锚点,优化长对话的相关性检索
// P1: API-dependent features 'EXTRACT_MEMORIES', // 自动从对话中提取并持久化记忆
'EXTRACT_MEMORIES', 'VERIFICATION_AGENT', // 验证代理,任务完成后自动校验结果
'VERIFICATION_AGENT', 'KAIROS_BRIEF', // Kairos 定时摘要(定时汇报当前状态)
'KAIROS_BRIEF', 'AWAY_SUMMARY', // 离线摘要(用户离开后生成总结)
'AWAY_SUMMARY', 'ULTRAPLAN', // 超级规划模式,深度分析后生成实施计划
'ULTRAPLAN', // 'DAEMON', // 守护进程模式,长驻 supervisor 管理后台 worker已禁用内存占用过高
// P2: daemon + remote control server 'ACP', // ACP 代理协议,支持外部 agent 接入
'DAEMON', 'WORKFLOW_SCRIPTS', // 工作流脚本(.claude/workflows/ 中的 YAML/MD
// ACP (Agent Client Protocol) agent mode 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口
'ACP', 'CONTEXT_COLLAPSE', // 上下文折叠,自动压缩旧消息
// PR-package restored features 'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出
'WORKFLOW_SCRIPTS', 'FORK_SUBAGENT', // Fork 子代理,在隔离上下文中并行执行任务
'HISTORY_SNIP', 'UDS_INBOX', // Unix Domain Socket 收件箱,跨会话消息传递
'CONTEXT_COLLAPSE', 'KAIROS', // Kairos 定时任务系统核心
'MONITOR_TOOL', 'COORDINATOR_MODE', // 协调者模式,多代理团队任务调度
'FORK_SUBAGENT', 'LAN_PIPES', // 局域网管道LAN 设备间通信
'UDS_INBOX', 'BG_SESSIONS', // 后台会话管理ps/logs/attach/kill
'KAIROS', 'TEMPLATES', // 模板任务new/list/reply 子命令)
'COORDINATOR_MODE', // 'REVIEW_ARTIFACT', // 代码审查产物API 请求无响应,待排查 schema 兼容性)
'LAN_PIPES',
'BG_SESSIONS',
'TEMPLATES',
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
// API content block types // API content block types
'CONNECTOR_TEXT', 'CONNECTOR_TEXT', // Connector 文本块类型,扩展 API 内容格式
// Attribution tracking // Attribution tracking
'COMMIT_ATTRIBUTION', 'COMMIT_ATTRIBUTION', // Git 提交归属追踪(记录 AI 辅助贡献)
// Server mode (claude server / claude open) // Server mode (claude server / claude open)
'DIRECT_CONNECT', 'DIRECT_CONNECT', // 直连模式claude server / claude open
// Skill search // Skill search
'EXPERIMENTAL_SKILL_SEARCH', 'EXPERIMENTAL_SKILL_SEARCH', // 实验性技能搜索DiscoverSkills
// P3: poor mode (disable extract_memories + prompt_suggestion) // P3: poor mode
'POOR', 'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
// Team Memory (shared memory files between agent teammates) // Team Memory
'TEAMMEM', 'TEAMMEM', // 团队记忆,代理队友间共享记忆文件
]as const; ]as const;

View File

@@ -86,9 +86,13 @@ import {
// Lazy: MessageSelector.tsx pulls React/ink; only needed for message filtering at query time // Lazy: MessageSelector.tsx pulls React/ink; only needed for message filtering at query time
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const messageSelector = const messageSelector = (): typeof import('src/components/MessageSelector.js') | null => {
(): typeof import('src/components/MessageSelector.js') => try {
require('src/components/MessageSelector.js') return require('src/components/MessageSelector.js')
} catch {
return null
}
}
import { import {
localCommandOutputToSDKAssistantMessage, localCommandOutputToSDKAssistantMessage,
@@ -466,12 +470,13 @@ export class QueryEngine {
} }
// Filter messages that should be acknowledged after transcript // Filter messages that should be acknowledged after transcript
const _selector = messageSelector()
const replayableMessages = messagesFromUserInput.filter( const replayableMessages = messagesFromUserInput.filter(
msg => msg =>
(msg.type === 'user' && (msg.type === 'user' &&
!msg.isMeta && // Skip synthetic caveat messages !msg.isMeta && // Skip synthetic caveat messages
!msg.toolUseResult && // Skip tool results (they'll be acked from query) !msg.toolUseResult && // Skip tool results (they'll be acked from query)
messageSelector().selectableUserMessagesFilter(msg)) || // Skip non-user-authored messages (task notifications, etc.) (_selector?.selectableUserMessagesFilter(msg) ?? true)) || // Skip non-user-authored messages (task notifications, etc.)
(msg.type === 'system' && msg.subtype === 'compact_boundary'), // Always ack compact boundaries (msg.type === 'system' && msg.subtype === 'compact_boundary'), // Always ack compact boundaries
) )
const messagesToAck = replayUserMessages ? replayableMessages : [] const messagesToAck = replayUserMessages ? replayableMessages : []
@@ -643,8 +648,10 @@ export class QueryEngine {
} }
if (fileHistoryEnabled() && persistSession) { if (fileHistoryEnabled() && persistSession) {
const _sel = messageSelector()
const _filter = _sel?.selectableUserMessagesFilter ?? ((_msg: unknown) => true)
messagesFromUserInput messagesFromUserInput
.filter(messageSelector().selectableUserMessagesFilter) .filter(_filter)
.forEach(message => { .forEach(message => {
void fileHistoryMakeSnapshot( void fileHistoryMakeSnapshot(
(updater: (prev: FileHistoryState) => FileHistoryState) => { (updater: (prev: FileHistoryState) => FileHistoryState) => {

View File

@@ -75,7 +75,7 @@ const bridge = feature('BRIDGE_MODE')
? require('./commands/bridge/index.js').default ? require('./commands/bridge/index.js').default
: null : null
const remoteControlServerCommand = const remoteControlServerCommand =
feature('DAEMON') && feature('BRIDGE_MODE') feature('BRIDGE_MODE')
? require('./commands/remoteControlServer/index.js').default ? require('./commands/remoteControlServer/index.js').default
: null : null
const voiceCommand = feature('VOICE_MODE') const voiceCommand = feature('VOICE_MODE')

View File

@@ -3,9 +3,14 @@ import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
import type { Command } from '../../commands.js' import type { Command } from '../../commands.js'
function isEnabled(): boolean { function isEnabled(): boolean {
if (!feature('DAEMON') || !feature('BRIDGE_MODE')) { if (!feature('BRIDGE_MODE')) {
return false return false
} }
if (feature('DAEMON')) {
return isBridgeEnabled()
}
// DAEMON feature disabled — still allow the command but warn at runtime
// that headless/daemon worker mode is unavailable.
return isBridgeEnabled() return isBridgeEnabled()
} }

View File

@@ -170,7 +170,12 @@ async function main(): Promise<void> {
// perf-sensitive. No enableConfigs(), no analytics sinks at this layer — // perf-sensitive. No enableConfigs(), no analytics sinks at this layer —
// workers are lean. If a worker kind needs configs/auth (assistant will), // workers are lean. If a worker kind needs configs/auth (assistant will),
// it calls them inside its run() fn. // it calls them inside its run() fn.
if (feature('DAEMON') && (args[0] === '--daemon-worker' || args[0]?.startsWith('--daemon-worker='))) { if (args[0] === '--daemon-worker' || args[0]?.startsWith('--daemon-worker=')) {
if (!feature('DAEMON')) {
console.error('Error: --daemon-worker requires DAEMON feature to be enabled. Set FEATURE_DAEMON=1 or add DAEMON to DEFAULT_BUILD_FEATURES.')
process.exitCode = 1
return
}
const kind = args[0] === '--daemon-worker' ? args[1] : args[0].split('=')[1] const kind = args[0] === '--daemon-worker' ? args[1] : args[0].split('=')[1]
const { runDaemonWorker } = await import('../daemon/workerRegistry.js') const { runDaemonWorker } = await import('../daemon/workerRegistry.js')
await runDaemonWorker(kind) await runDaemonWorker(kind)

View File

@@ -12,29 +12,19 @@
*/ */
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle'
import { useEffect } from 'react' import { useEffect } from 'react'
import * as pt from '../utils/pipeTransport.js'
import * as pr from '../utils/pipeRegistry.js'
import * as mm from './useMasterMonitor.js'
import { getSessionId as _getSessionId } from '../bootstrap/state.js'
import * as lb from '../utils/lanBeacon.js'
import * as pp from '../utils/pipePermissionRelay.js'
import * as osm from 'os'
import type { import type {
PipeMessage, PipeMessage,
PipeServer, PipeServer,
PipeIpcState, PipeIpcState,
} from '../utils/pipeTransport.js' } from '../utils/pipeTransport.js'
// Lazy-loaded module accessors (cached by Bun/Node require)
/* eslint-disable @typescript-eslint/no-require-imports */
const pt = () =>
require('../utils/pipeTransport.js') as typeof import('../utils/pipeTransport.js')
const pr = () =>
require('../utils/pipeRegistry.js') as typeof import('../utils/pipeRegistry.js')
const mm = () =>
require('./useMasterMonitor.js') as typeof import('./useMasterMonitor.js')
const bs = () =>
require('../bootstrap/state.js') as typeof import('../bootstrap/state.js')
const lb = () =>
require('../utils/lanBeacon.js') as typeof import('../utils/lanBeacon.js')
const pp = () =>
require('../utils/pipePermissionRelay.js') as typeof import('../utils/pipePermissionRelay.js')
const osm = () => require('os') as typeof import('os')
/* eslint-enable @typescript-eslint/no-require-imports */
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -54,9 +44,9 @@ export type UsePipeIpcOptions = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function removeDeadSlave(slaveName: string, store: StoreApi): void { function removeDeadSlave(slaveName: string, store: StoreApi): void {
mm().removeSlaveClient(slaveName) mm.removeSlaveClient(slaveName)
store.setState((prev: any) => { store.setState((prev: any) => {
const pipeIpc = pt().getPipeIpc(prev) const pipeIpc = pt.getPipeIpc(prev)
const { [slaveName]: _removed, ...remainingSlaves } = pipeIpc.slaves const { [slaveName]: _removed, ...remainingSlaves } = pipeIpc.slaves
return { return {
...prev, ...prev,
@@ -108,7 +98,7 @@ function refreshDiscoveredPipes(
// Include LAN beacon peers so they aren't wiped out by heartbeat // Include LAN beacon peers so they aren't wiped out by heartbeat
let lanDiscovered: typeof freshDiscovered = [] let lanDiscovered: typeof freshDiscovered = []
if (feature('LAN_PIPES')) { if (feature('LAN_PIPES')) {
const beacon = lb().getLanBeacon() const beacon = lb.getLanBeacon()
if (beacon) { if (beacon) {
const localNames = new Set(freshDiscovered.map(p => p.pipeName)) const localNames = new Set(freshDiscovered.map(p => p.pipeName))
localNames.add(pipeName) localNames.add(pipeName)
@@ -131,7 +121,7 @@ function refreshDiscoveredPipes(
const allDiscovered = [...freshDiscovered, ...lanDiscovered] const allDiscovered = [...freshDiscovered, ...lanDiscovered]
// Only update state if the list actually changed // Only update state if the list actually changed
const prev = pt().getPipeIpc(store.getState()) const prev = pt.getPipeIpc(store.getState())
const prevNames = (prev.discoveredPipes ?? []) const prevNames = (prev.discoveredPipes ?? [])
.map((p: any) => p.pipeName) .map((p: any) => p.pipeName)
.join(',') .join(',')
@@ -139,7 +129,7 @@ function refreshDiscoveredPipes(
if (prevNames === newNames) return if (prevNames === newNames) return
store.setState((prev: any) => { store.setState((prev: any) => {
const pipeIpc = pt().getPipeIpc(prev) const pipeIpc = pt.getPipeIpc(prev)
const aliveNames = new Set(allDiscovered.map(pipe => pipe.pipeName)) const aliveNames = new Set(allDiscovered.map(pipe => pipe.pipeName))
return { return {
...prev, ...prev,
@@ -174,8 +164,8 @@ function registerMessageHandlers(
server.onMessage((msg: PipeMessage, reply) => { server.onMessage((msg: PipeMessage, reply) => {
if (msg.type !== 'attach_request') return if (msg.type !== 'attach_request') return
const state = store.getState() const state = store.getState()
const currentPipeState = pt().getPipeIpc(state) const currentPipeState = pt.getPipeIpc(state)
if (pt().isPipeControlled(currentPipeState)) { if (pt.isPipeControlled(currentPipeState)) {
reply({ type: 'attach_reject', data: 'Already controlled' }) reply({ type: 'attach_reject', data: 'Already controlled' })
return return
} }
@@ -192,7 +182,7 @@ function registerMessageHandlers(
const clients = Array.from((server as any).clients as Set<any>) const clients = Array.from((server as any).clients as Set<any>)
const masterSocket = clients[clients.length - 1] const masterSocket = clients[clients.length - 1]
pp().setPipeRelay((relayMsg: any) => { pp.setPipeRelay((relayMsg: any) => {
if (masterSocket && !masterSocket.destroyed) { if (masterSocket && !masterSocket.destroyed) {
relayMsg.from = relayMsg.from ?? pipeName relayMsg.from = relayMsg.from ?? pipeName
relayMsg.ts = relayMsg.ts ?? new Date().toISOString() relayMsg.ts = relayMsg.ts ?? new Date().toISOString()
@@ -203,9 +193,9 @@ function registerMessageHandlers(
store.setState((prev: any) => ({ store.setState((prev: any) => ({
...prev, ...prev,
pipeIpc: { pipeIpc: {
...pt().getPipeIpc(prev), ...pt.getPipeIpc(prev),
role: 'sub', role: 'sub',
displayRole: pt().getPipeDisplayRole(pt().getPipeIpc(prev)), displayRole: pt.getPipeDisplayRole(pt.getPipeIpc(prev)),
attachedBy: msg.from ?? 'unknown', attachedBy: msg.from ?? 'unknown',
}, },
})) }))
@@ -230,8 +220,7 @@ function registerMessageHandlers(
server.onMessage((msg: PipeMessage, _reply) => { server.onMessage((msg: PipeMessage, _reply) => {
if (msg.type !== 'permission_response' && msg.type !== 'permission_cancel') if (msg.type !== 'permission_response' && msg.type !== 'permission_cancel')
return return
const { resolvePipePermissionResponse, cancelPipePermissionRequest } = const { resolvePipePermissionResponse, cancelPipePermissionRequest } = pp
require('../utils/pipePermissionRelay.js') as typeof import('../utils/pipePermissionRelay.js')
try { try {
const payload = msg.data ? JSON.parse(msg.data) : undefined const payload = msg.data ? JSON.parse(msg.data) : undefined
@@ -249,28 +238,27 @@ function registerMessageHandlers(
// Handle relay mute/unmute from master // Handle relay mute/unmute from master
server.onMessage((msg: PipeMessage, _reply) => { server.onMessage((msg: PipeMessage, _reply) => {
if (msg.type === 'relay_mute') { if (msg.type === 'relay_mute') {
pp().setRelayMuted(true) pp.setRelayMuted(true)
} else if (msg.type === 'relay_unmute') { } else if (msg.type === 'relay_unmute') {
pp().setRelayMuted(false) pp.setRelayMuted(false)
} }
}) })
// Handle detach // Handle detach
server.onMessage((msg: PipeMessage, _reply) => { server.onMessage((msg: PipeMessage, _reply) => {
if (msg.type !== 'detach') return if (msg.type !== 'detach') return
const { clearPendingPipePermissions } = const { clearPendingPipePermissions } = pp
require('../utils/pipePermissionRelay.js') as typeof import('../utils/pipePermissionRelay.js')
clearPendingPipePermissions('Pipe detached before permission was resolved.') clearPendingPipePermissions('Pipe detached before permission was resolved.')
pp().setPipeRelay(null) pp.setPipeRelay(null)
store.setState((prev: any) => ({ store.setState((prev: any) => ({
...prev, ...prev,
pipeIpc: (() => { pipeIpc: (() => {
const pipeIpc = pt().getPipeIpc(prev) const pipeIpc = pt.getPipeIpc(prev)
const nextRole = pipeIpc.subIndex != null ? 'sub' : 'main' const nextRole = pipeIpc.subIndex != null ? 'sub' : 'main'
const nextPipeState = { ...pipeIpc, role: nextRole, attachedBy: null } const nextPipeState = { ...pipeIpc, role: nextRole, attachedBy: null }
return { return {
...nextPipeState, ...nextPipeState,
displayRole: pt().getPipeDisplayRole(nextPipeState as PipeIpcState), displayRole: pt.getPipeDisplayRole(nextPipeState as PipeIpcState),
} }
})(), })(),
})) }))
@@ -289,11 +277,11 @@ function runMainHeartbeat(
): void { ): void {
void (async () => { void (async () => {
try { try {
await pr().cleanupStaleEntries() await pr.cleanupStaleEntries()
const aliveSubs = await pr().getAliveSubs() const aliveSubs = await pr.getAliveSubs()
refreshDiscoveredPipes(pipeName, aliveSubs, store) refreshDiscoveredPipes(pipeName, aliveSubs, store)
const connectedSlaves = mm().getAllSlaveClients() const connectedSlaves = mm.getAllSlaveClients()
const aliveSubNames = new Set(aliveSubs.map(sub => sub.pipeName)) const aliveSubNames = new Set(aliveSubs.map(sub => sub.pipeName))
// Build unified attach target list: local subs + LAN peers // Build unified attach target list: local subs + LAN peers
@@ -307,7 +295,7 @@ function runMainHeartbeat(
// Add LAN peers as attach targets // Add LAN peers as attach targets
if (feature('LAN_PIPES')) { if (feature('LAN_PIPES')) {
const beacon = lb().getLanBeacon() const beacon = lb.getLanBeacon()
if (beacon) { if (beacon) {
const localNames = new Set(attachTargets.map(t => t.pipeName)) const localNames = new Set(attachTargets.map(t => t.pipeName))
localNames.add(pipeName) localNames.add(pipeName)
@@ -323,7 +311,7 @@ function runMainHeartbeat(
} }
} }
const currentPipeState = pt().getPipeIpc(store.getState()) const currentPipeState = pt.getPipeIpc(store.getState())
for (const target of attachTargets) { for (const target of attachTargets) {
if (target.pipeName === pipeName) continue if (target.pipeName === pipeName) continue
@@ -331,7 +319,7 @@ function runMainHeartbeat(
try { try {
const myName = currentPipeState.serverName ?? pipeName const myName = currentPipeState.serverName ?? pipeName
const client = await pt().connectToPipe( const client = await pt.connectToPipe(
target.pipeName, target.pipeName,
myName, myName,
3000, 3000,
@@ -362,7 +350,7 @@ function runMainHeartbeat(
}) })
if (attached && !disposed.current) { if (attached && !disposed.current) {
mm().addSlaveClient(target.pipeName, client) mm.addSlaveClient(target.pipeName, client)
client.on('disconnect', () => { client.on('disconnect', () => {
removeDeadSlave(target.pipeName, store) removeDeadSlave(target.pipeName, store)
@@ -371,11 +359,11 @@ function runMainHeartbeat(
store.setState((prev: any) => ({ store.setState((prev: any) => ({
...prev, ...prev,
pipeIpc: { pipeIpc: {
...pt().getPipeIpc(prev), ...pt.getPipeIpc(prev),
role: 'master', role: 'master',
displayRole: 'master', displayRole: 'master',
slaves: { slaves: {
...pt().getPipeIpc(prev).slaves, ...pt.getPipeIpc(prev).slaves,
[target.pipeName]: { [target.pipeName]: {
name: target.pipeName, name: target.pipeName,
connectedAt: new Date().toISOString(), connectedAt: new Date().toISOString(),
@@ -395,7 +383,7 @@ function runMainHeartbeat(
// Clean up slaves that are no longer alive // Clean up slaves that are no longer alive
let lanPeerNames: Set<string> | null = null let lanPeerNames: Set<string> | null = null
if (feature('LAN_PIPES')) { if (feature('LAN_PIPES')) {
const beacon = lb().getLanBeacon() const beacon = lb.getLanBeacon()
if (beacon) { if (beacon) {
lanPeerNames = new Set(beacon.getPeers().keys()) lanPeerNames = new Set(beacon.getPeers().keys())
} }
@@ -422,28 +410,28 @@ function runSubHeartbeat(
): void { ): void {
void (async () => { void (async () => {
try { try {
const mainAlive = await pr().isMainAlive() const mainAlive = await pr.isMainAlive()
if (!mainAlive && !disposed.current) { if (!mainAlive && !disposed.current) {
const registry = await pr().readRegistry() const registry = await pr.readRegistry()
const isSameMachine = pr().isMainMachine(machineId, registry) const isSameMachine = pr.isMainMachine(machineId, registry)
if (isSameMachine) { if (isSameMachine) {
await pr().registerAsMain(entry) await pr.registerAsMain(entry)
} else { } else {
await pr().revertToIndependent(pipeName) await pr.revertToIndependent(pipeName)
} }
store.setState((prev: any) => ({ store.setState((prev: any) => ({
...prev, ...prev,
pipeIpc: { pipeIpc: {
...pt().getPipeIpc(prev), ...pt.getPipeIpc(prev),
role: 'main', role: 'main',
subIndex: null, subIndex: null,
displayRole: 'main', displayRole: 'main',
attachedBy: null, attachedBy: null,
}, },
})) }))
pp().setPipeRelay(null) pp.setPipeRelay(null)
} }
} catch { } catch {
// Heartbeat check error — non-fatal // Heartbeat check error — non-fatal
@@ -462,7 +450,9 @@ export function usePipeIpc({
if (!feature('UDS_INBOX')) return if (!feature('UDS_INBOX')) return
useEffect(() => { useEffect(() => {
const pipeName = `cli-${bs().getSessionId().slice(0, 8)}` const sessionId = _getSessionId()
if (!sessionId) return
const pipeName = `cli-${sessionId.slice(0, 8)}`
const disposed = { current: false } const disposed = { current: false }
let heartbeatTimer: ReturnType<typeof setInterval> | null = null let heartbeatTimer: ReturnType<typeof setInterval> | null = null
let heartbeatBusy = false let heartbeatBusy = false
@@ -471,11 +461,11 @@ export function usePipeIpc({
void (async () => { void (async () => {
try { try {
// --- Phase 1: Role determination --- // --- Phase 1: Role determination ---
const machId = await pr().getMachineId() const machId = await pr.getMachineId()
const mac = pr().getMacAddress() const mac = pr.getMacAddress()
const localIp = pt().getLocalIp() const localIp = pt.getLocalIp()
const host = osm().hostname() const host = osm.hostname()
const roleResult = await pr().determineRole(machId) const roleResult = await pr.determineRole(machId)
const entry = { const entry = {
id: pipeName, id: pipeName,
@@ -493,29 +483,29 @@ export function usePipeIpc({
let displayRole = 'main' let displayRole = 'main'
if (roleResult.role === 'main' || roleResult.role === 'main-recover') { if (roleResult.role === 'main' || roleResult.role === 'main-recover') {
await pr().registerAsMain(entry) await pr.registerAsMain(entry)
} else { } else {
subIndex = roleResult.subIndex subIndex = roleResult.subIndex
await pr().registerAsSub(entry, subIndex) await pr.registerAsSub(entry, subIndex)
initialRole = 'sub' initialRole = 'sub'
displayRole = `sub-${subIndex}` displayRole = `sub-${subIndex}`
} }
// --- Phase 2: Server creation --- // --- Phase 2: Server creation ---
const server = await pt().createPipeServer( const server = await pt.createPipeServer(
pipeName, pipeName,
feature('LAN_PIPES') ? { enableTcp: true, tcpPort: 0 } : undefined, feature('LAN_PIPES') ? { enableTcp: true, tcpPort: 0 } : undefined,
) )
pipeServer = server pipeServer = server
if (disposed.current) { if (disposed.current) {
await server.close() await server.close()
await pr().unregister(pipeName) await pr.unregister(pipeName)
return return
} }
// --- Phase 3: LAN beacon --- // --- Phase 3: LAN beacon ---
if (feature('LAN_PIPES') && server.tcpAddress) { if (feature('LAN_PIPES') && server.tcpAddress) {
const beacon = new (lb().LanBeacon)({ const beacon = new (lb.LanBeacon)({
pipeName, pipeName,
machineId: machId, machineId: machId,
hostname: host, hostname: host,
@@ -524,7 +514,7 @@ export function usePipeIpc({
role: initialRole, role: initialRole,
}) })
beacon.start() beacon.start()
lb().setLanBeacon(beacon) lb.setLanBeacon(beacon)
const entryWithTcp = { const entryWithTcp = {
...entry, ...entry,
@@ -532,9 +522,9 @@ export function usePipeIpc({
lanVisible: true, lanVisible: true,
} }
if (initialRole === 'main') { if (initialRole === 'main') {
await pr().registerAsMain(entryWithTcp) await pr.registerAsMain(entryWithTcp)
} else if (subIndex != null) { } else if (subIndex != null) {
await pr().registerAsSub(entryWithTcp, subIndex) await pr.registerAsSub(entryWithTcp, subIndex)
} }
} }
@@ -542,7 +532,7 @@ export function usePipeIpc({
store.setState((prev: any) => ({ store.setState((prev: any) => ({
...prev, ...prev,
pipeIpc: { pipeIpc: {
...pt().getPipeIpc(prev), ...pt.getPipeIpc(prev),
serverName: pipeName, serverName: pipeName,
role: initialRole, role: initialRole,
subIndex, subIndex,
@@ -570,7 +560,7 @@ export function usePipeIpc({
if (disposed.current || heartbeatBusy) return if (disposed.current || heartbeatBusy) return
heartbeatBusy = true heartbeatBusy = true
const currentPipeState = pt().getPipeIpc(store.getState()) const currentPipeState = pt.getPipeIpc(store.getState())
if ( if (
currentPipeState.role === 'main' || currentPipeState.role === 'main' ||
@@ -600,7 +590,7 @@ export function usePipeIpc({
} }
// Send detach to all slaves // Send detach to all slaves
const allClients = mm().getAllSlaveClients() const allClients = mm.getAllSlaveClients()
for (const [name, client] of allClients.entries()) { for (const [name, client] of allClients.entries()) {
try { try {
client.send({ type: 'detach' }) client.send({ type: 'detach' })
@@ -610,23 +600,21 @@ export function usePipeIpc({
} }
// Stop LAN beacon // Stop LAN beacon
const beacon = lb().getLanBeacon() const beacon = lb.getLanBeacon()
if (beacon) { if (beacon) {
try { try {
beacon.stop() beacon.stop()
} catch {} } catch {}
lb().setLanBeacon(null) lb.setLanBeacon(null)
} }
// Unregister + close server // Unregister + close server
void pr() pr.unregister(pipeName).catch(() => {})
.unregister(pipeName)
.catch(() => {})
if (pipeServer) { if (pipeServer) {
void pipeServer.close().catch(() => {}) void pipeServer.close().catch(() => {})
pipeServer = null pipeServer = null
} }
pp().setPipeRelay(null) pp.setPipeRelay(null)
} }
}, []) // eslint-disable-line react-hooks/exhaustive-deps }, []) // eslint-disable-line react-hooks/exhaustive-deps
} }