mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
323 lines
14 KiB
TypeScript
323 lines
14 KiB
TypeScript
import {
|
|
captureStructuredAgentCliSource,
|
|
structuredAgentCliSourcePathFromEvidence
|
|
} from '../../native-chat/structured-agent-cli-source'
|
|
import type { OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce as RestoreStructuredAgentSessionTabsOnceMethods } from './structured-session-tab-restoration'
|
|
import { agentSessionRefusalError } from '../../../shared/agent-session-wire-refusals'
|
|
import { OrcaRuntimeWithStructuredSessionAccountHome } from './structured-session-account-home'
|
|
import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/structured-agent-session-registry'
|
|
import {
|
|
resolveCommittedStructuredAgentSessionAdoptionIntent,
|
|
requireUnownedStructuredAgentSessionAdoption,
|
|
resolveStructuredAgentSessionAdoptionForCreate,
|
|
structuredAgentSessionAdoptAttach,
|
|
type StructuredAgentSessionCreateIntentInput
|
|
} from '../structured-agent-session-create-adoption'
|
|
import { isNativeLocalLocation, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
|
import { getLocalProjectWorktreeGitOptions } from '../../project-runtime-git-options'
|
|
import type { AgentSessionAttachParams } from '../../native-chat/agent-session-wire/structured-agent-session-attach'
|
|
import {
|
|
resolveHostStructuredAgentCreateSupport,
|
|
resolveHostStructuredAgentLaunchEnvironment
|
|
} from '../structured-agent-launch-support'
|
|
import { structuredAgentRuntimeMetadata } from '../structured-agent-runtime-metadata'
|
|
import { structuredAgentChatProgramNeedsInstall } from '../structured-agent-availability'
|
|
import { structuredLaunchSeedOptions } from '../../../shared/native-chat/input/native-chat-session-option-defaults'
|
|
import { hasPersistedStructuredAgentSessionStore as hasPersistedStructuredAgentSessionStoreOnDisk } from '../structured-agent-session-runtime'
|
|
import { ensureStructuredAgentSessionHostUnlessRefused } from '../structured-agent-session-host-refusal'
|
|
import { getProfileUserDataPath } from '../../orca-profiles/profile-storage-paths'
|
|
import { parseWslUncPath } from '../../../shared/wsl-paths'
|
|
import {
|
|
isFloatingWorkspaceId,
|
|
isFloatingWorkspaceSelector
|
|
} from '../../../shared/floating-workspace-worktree'
|
|
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
|
import type { AgentSessionAccountHome } from '../../../shared/agent-session-account-home'
|
|
import type { StructuredAgentId } from '../../../shared/agent-session-provider-handle'
|
|
|
|
export abstract class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaRuntimeWithStructuredSessionAccountHome {
|
|
protected abstract restoreStructuredAgentSessionTabsOnce(
|
|
...args: Parameters<
|
|
RestoreStructuredAgentSessionTabsOnceMethods['restoreStructuredAgentSessionTabsOnce']
|
|
>
|
|
): ReturnType<
|
|
RestoreStructuredAgentSessionTabsOnceMethods['restoreStructuredAgentSessionTabsOnce']
|
|
>
|
|
|
|
/** `requireInstalledChatProgram` is for launches with no chat tab to offer Orca's install. */
|
|
async getStructuredAgentSessionCreateSupport(
|
|
worktreeSelector: string,
|
|
agent: StructuredAgentId,
|
|
options?: { requireInstalledChatProgram?: boolean }
|
|
): Promise<{ supported: boolean; reason?: 'agent' | 'remote' | 'wsl' | 'chat-program' }> {
|
|
const support = await resolveHostStructuredAgentCreateSupport({
|
|
agent,
|
|
worktreeSelector,
|
|
location: await this.resolveStructuredAgentSessionLocation(worktreeSelector),
|
|
runtime: this
|
|
})
|
|
if (
|
|
support.supported &&
|
|
options?.requireInstalledChatProgram &&
|
|
(await structuredAgentChatProgramNeedsInstall(agent, this))
|
|
) {
|
|
return { supported: false, reason: 'chat-program' }
|
|
}
|
|
return support
|
|
}
|
|
|
|
/** Use this runtime's registered definition to pin account home; reads/create must not install the host just to inspect it. */
|
|
protected requireRegisteredStructuredAgent(agent: StructuredAgentId) {
|
|
const definition = structuredAgentRuntimeMetadata(agent)?.definition
|
|
if (!definition) {
|
|
throw agentSessionRefusalError('structured_agent_session_unsupported', {
|
|
reason: 'hostUnsupported'
|
|
})
|
|
}
|
|
return definition
|
|
}
|
|
|
|
/** Share new-chat selection between create and createSupport so client pickers show what create will run. */
|
|
structuredAgentSessionLaunchSeedOptions(
|
|
agent: StructuredAgentId
|
|
): Record<string, string> | undefined {
|
|
return structuredLaunchSeedOptions(this.requireStore().getSettings(), agent)
|
|
}
|
|
|
|
protected async resolveStructuredAgentSessionLocation(worktreeSelector: string) {
|
|
const target = await this.resolveRuntimeFileTarget(worktreeSelector)
|
|
const repo = this.store?.getRepo(target.worktree.repoId)
|
|
const folderScope = parseWorkspaceKey(target.worktree.id)
|
|
// Floating workspaces are plain managed directories; folder describes management, not whether they contain Git.
|
|
const folderWorkspace =
|
|
folderScope?.type === 'folder' || isFloatingWorkspaceId(target.worktree.id)
|
|
// WSL routing is local-machine authority; use executionHostId so remote/runtime hosts cannot inherit it.
|
|
const isLocalHost = target.executionHostId === LOCAL_EXECUTION_HOST_ID
|
|
const configuredWslDistro =
|
|
repo && isLocalHost
|
|
? (getLocalProjectWorktreeGitOptions(this.requireStore(), repo).wslDistro ?? null)
|
|
: null
|
|
// Folder workspaces lack repo Git options; WSL UNC paths are their durable signal to refuse native Windows Codex.
|
|
const wslDistro =
|
|
configuredWslDistro ??
|
|
(folderWorkspace && isLocalHost
|
|
? (parseWslUncPath(target.worktree.path)?.distro ?? null)
|
|
: null)
|
|
return {
|
|
executionHostId: target.executionHostId,
|
|
wslDistro,
|
|
workspaceId: target.worktree.id,
|
|
workspaceKind: folderWorkspace ? ('folder' as const) : ('git-worktree' as const)
|
|
}
|
|
}
|
|
|
|
/** Where a structured chat here would run, when that is a directory on this machine. */
|
|
async resolveStructuredAgentSessionLocalWorkspacePath(worktreeSelector: string) {
|
|
const location = await this.resolveStructuredAgentSessionLocation(worktreeSelector)
|
|
if (!isNativeLocalLocation(location)) {
|
|
return null
|
|
}
|
|
return (await this.resolveRuntimeFileTarget(worktreeSelector)).worktree.path
|
|
}
|
|
|
|
async resolveStructuredAgentSessionCreateIntent(
|
|
input: StructuredAgentSessionCreateIntentInput
|
|
): Promise<AgentSessionAttachParams & { hostLaunchDirectory?: string }> {
|
|
if (input.resumeFrom) {
|
|
const { providerSessionId, cliSource } = input.resumeFrom
|
|
try {
|
|
requireUnownedStructuredAgentSessionAdoption({
|
|
host: getStructuredAgentSessionHost(),
|
|
agent: input.agent,
|
|
providerSessionId,
|
|
selfSessionId: input.envelope.sessionId
|
|
})
|
|
} catch (ownerConflict) {
|
|
// Refuse transfer from a live terminal or the chat's first send starts a second writer.
|
|
await this.assertStructuredAdoptionTerminalWritersExited(
|
|
input.agent,
|
|
providerSessionId,
|
|
cliSource
|
|
)
|
|
throw ownerConflict
|
|
}
|
|
}
|
|
const structuredAgentCliSourceEvidence =
|
|
input.structuredAgentCliSourceEvidence ??
|
|
(input.resumeFrom?.cliSource
|
|
? captureStructuredAgentCliSource(() => this.getAgentProviderSessionRows())
|
|
: undefined)
|
|
const hostLaunchDirectory = isFloatingWorkspaceSelector(input.worktree)
|
|
? (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path
|
|
: undefined
|
|
const resolveAccountHome = this.structuredAgentAccountHomeResolver(
|
|
input.agent,
|
|
input.worktree,
|
|
'launch',
|
|
hostLaunchDirectory
|
|
)
|
|
if (!resolveAccountHome) {
|
|
throw agentSessionRefusalError('structured_agent_session_unsupported', {
|
|
reason: 'hostUnsupported'
|
|
})
|
|
}
|
|
const resolved = await this.resolveStructuredAgentSessionIntent(
|
|
{ ...input, structuredAgentCliSourceEvidence },
|
|
resolveAccountHome
|
|
)
|
|
return hostLaunchDirectory ? { ...resolved, hostLaunchDirectory } : resolved
|
|
}
|
|
|
|
/** Resolve catalog account home through the same host create-intent resolver when no session record exists. */
|
|
async resolveStructuredAgentAccountHome(
|
|
agent: StructuredAgentId
|
|
): Promise<AgentSessionAccountHome> {
|
|
const resolveAccountHome = this.structuredAgentAccountHomeResolver(agent, '', 'read')
|
|
if (!resolveAccountHome) {
|
|
throw agentSessionRefusalError('structured_agent_session_unsupported', {
|
|
reason: 'hostUnsupported'
|
|
})
|
|
}
|
|
const launchEnv = this.structuredAgentLaunchEnv(agent, this.requireStore().getSettings())
|
|
return resolveAccountHome({ launchEnv, location: null })
|
|
}
|
|
|
|
protected async resolveStructuredAgentSessionIntent(
|
|
input: StructuredAgentSessionCreateIntentInput,
|
|
resolveAccountHome: (context: {
|
|
launchEnv: NodeJS.ProcessEnv
|
|
location: {
|
|
executionHostId: string
|
|
wslDistro: string | null
|
|
workspaceId: string
|
|
workspaceKind: 'folder' | 'git-worktree'
|
|
}
|
|
}) => AgentSessionAccountHome | Promise<AgentSessionAccountHome>
|
|
): Promise<AgentSessionAttachParams> {
|
|
const support = await this.getStructuredAgentSessionCreateSupport(input.worktree, input.agent)
|
|
// Adopting a conversation reads the agent's own transcript, so only an agent registered with an
|
|
// importer adopts.
|
|
const transcriptImport = structuredAgentRuntimeMetadata(input.agent)?.transcriptImport
|
|
if (!support.supported || (input.resumeFrom && !transcriptImport)) {
|
|
throw agentSessionRefusalError('structured_agent_session_unsupported', {
|
|
reason: 'hostUnsupported'
|
|
})
|
|
}
|
|
const settings = this.requireStore().getSettings()
|
|
const launchEnv = this.structuredAgentLaunchEnv(input.agent, settings)
|
|
const options = this.structuredAgentSessionLaunchSeedOptions(input.agent)
|
|
const location = await this.resolveStructuredAgentSessionLocation(input.worktree)
|
|
const definition = this.requireRegisteredStructuredAgent(input.agent)
|
|
const host = getStructuredAgentSessionHost()
|
|
const committedReplay = resolveCommittedStructuredAgentSessionAdoptionIntent({
|
|
host,
|
|
...input,
|
|
location,
|
|
...(options ? { options } : {})
|
|
})
|
|
if (committedReplay) {
|
|
return committedReplay
|
|
}
|
|
const resumeFrom = input.resumeFrom
|
|
if (resumeFrom) {
|
|
await this.assertStructuredAdoptionTerminalWritersExited(
|
|
input.agent,
|
|
resumeFrom.providerSessionId,
|
|
resumeFrom.cliSource
|
|
)
|
|
}
|
|
const workspaceCwd = resumeFrom
|
|
? (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path
|
|
: ''
|
|
const selectedAccountHome = await resolveAccountHome({ launchEnv, location })
|
|
// Adoption pins the transcript's actual account home; fresh-create defaults may open a blank conversation under the old name.
|
|
const adoption =
|
|
resumeFrom && transcriptImport
|
|
? await resolveStructuredAgentSessionAdoptionForCreate({
|
|
settings,
|
|
agent: input.agent,
|
|
transcriptImport,
|
|
providerSessionId: resumeFrom.providerSessionId,
|
|
resolveTranscriptPath: () =>
|
|
structuredAgentCliSourcePathFromEvidence({
|
|
agent: input.agent,
|
|
resumeFrom,
|
|
workspaceId: location.workspaceId,
|
|
evidence: input.structuredAgentCliSourceEvidence
|
|
}),
|
|
prepareCodexResume: this.prepareAiVaultSessionResumeFn ?? undefined,
|
|
selectedAccountHome,
|
|
launchEnv: await resolveHostStructuredAgentLaunchEnvironment(input.agent, settings),
|
|
workspaceCwd: workspaceCwd
|
|
})
|
|
: null
|
|
return {
|
|
envelope: {
|
|
sessionId: input.envelope.sessionId,
|
|
clientOperationId: input.envelope.clientOperationId,
|
|
expectedRuntimeFence: null,
|
|
payloadFingerprint: ''
|
|
},
|
|
location,
|
|
provider: input.agent,
|
|
agent: input.agent,
|
|
accountHome: adoption ? adoption.accountHome : selectedAccountHome,
|
|
...(options ? { options } : {}),
|
|
...(input.resumeFrom && adoption
|
|
? {
|
|
adopt: structuredAgentSessionAdoptAttach({
|
|
definition,
|
|
handleNamesTranscript: transcriptImport!.handleNamesTranscript,
|
|
transcriptPath: adoption.transcriptPath,
|
|
providerSessionId: input.resumeFrom.providerSessionId,
|
|
workspaceCwd
|
|
})
|
|
}
|
|
: {}),
|
|
runtimeKind: 'native'
|
|
}
|
|
}
|
|
|
|
restoreStructuredAgentSessionTabs(): Promise<void> {
|
|
this.structuredAgentSessionTabRestorePromise ??=
|
|
this.restoreStructuredAgentSessionTabsOnce().then(
|
|
() => {
|
|
// Only host answers finalize restoration; without one, retry so journals opened later can publish.
|
|
if (this.structuredAgentSessionInventoryUnverifiable) {
|
|
this.structuredAgentSessionTabRestorePromise = null
|
|
}
|
|
},
|
|
(error) => {
|
|
this.structuredAgentSessionTabRestorePromise = null
|
|
throw error
|
|
}
|
|
)
|
|
return this.structuredAgentSessionTabRestorePromise
|
|
}
|
|
|
|
prepareStructuredAgentSessionStartupRestoration(): Promise<void> {
|
|
this.structuredAgentSessionStartupRestorePromise ??=
|
|
this.prepareStructuredAgentSessionStartupRestorationOnce().catch((error) => {
|
|
this.structuredAgentSessionStartupRestorePromise = null
|
|
throw error
|
|
})
|
|
return this.structuredAgentSessionStartupRestorePromise
|
|
}
|
|
|
|
protected async prepareStructuredAgentSessionStartupRestorationOnce(): Promise<void> {
|
|
if (!this.hasPersistedStructuredAgentSessionStore()) {
|
|
return
|
|
}
|
|
// Load durable agent records before reconciling daemon inventory; refused hosts block only structured requests, not app startup.
|
|
await ensureStructuredAgentSessionHostUnlessRefused(() =>
|
|
this.ensureStructuredAgentSessionHost()
|
|
)
|
|
await this.refreshMobileSessionPtyRecords()
|
|
await getStructuredAgentSessionHost()?.reconcileRestartLeases()
|
|
}
|
|
|
|
protected hasPersistedStructuredAgentSessionStore(): boolean {
|
|
return hasPersistedStructuredAgentSessionStoreOnDisk(getProfileUserDataPath())
|
|
}
|
|
}
|