Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/scripts/electron-vite-output-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ describe('Electron Vite output contract', () => {
expect(external('zod', undefined, false)).toBe(false)
expect(external('smol-toml', undefined, false)).toBe(false)
expect(external('smol-toml/package.json', undefined, false)).toBe(false)
expect(external('@noble/hashes/blake3', undefined, false)).toBe(false)
expect(external('@noble/hashes/utils', undefined, false)).toBe(false)
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('smol-toml')
Expand Down
12 changes: 11 additions & 1 deletion src/main/acp/acp-launch-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ import {
loadOpenCodeVisualsSkill,
type AcpVisualsSkillLoader
} from './acp-visuals-skill'
import type { StructuredAgentSessionHistory } from '../native-chat/structured-agent-cli-conversations'
import type {
StructuredAgentSessionHistory,
StructuredAgentTranscriptImport
} from '../native-chat/structured-agent-cli-conversations'
import { GROK_SESSION_HISTORY } from '../grok/grok-session-history'
import { GROK_TRANSCRIPT_IMPORT } from '../grok/grok-transcript-import'
import { OPENCODE_SESSION_HISTORY } from '../opencode/opencode-session-history'
import { OMP_SESSION_HISTORY } from '../omp/omp-session-history'
import { OMP_TRANSCRIPT_IMPORT } from '../omp/omp-transcript-import'

/** How an agent lists its models without a session: from its `initialize` answer (plus extension
* requests), from a listing command, or not at all. Never `authenticate` or `session/new`. */
Expand Down Expand Up @@ -91,6 +96,8 @@ export type AcpLaunchSpec = {
modelDiscovery: AcpModelDiscovery
/** How a launch loads the inline-visuals skill; absent, the agent's chats have no visuals. */
visualsSkill?: AcpVisualsSkillLoader
/** How a chat adopts a conversation the agent's CLI recorded; null when it cannot. */
transcriptImport: StructuredAgentTranscriptImport | null
/** How the agent's chats own the Session History rows their conversations list as. */
sessionHistory: StructuredAgentSessionHistory
}
Expand Down Expand Up @@ -126,6 +133,7 @@ const GROK_LAUNCH_SPEC: AcpLaunchSpec = {
},
visualsSkill: loadGrokVisualsSkill,
compaction: true,
transcriptImport: GROK_TRANSCRIPT_IMPORT,
sessionHistory: GROK_SESSION_HISTORY
}

Expand Down Expand Up @@ -165,6 +173,7 @@ const OPENCODE_LAUNCH_SPEC: AcpLaunchSpec = {
},
visualsSkill: loadOpenCodeVisualsSkill,
compaction: true,
transcriptImport: null,
sessionHistory: OPENCODE_SESSION_HISTORY
}

Expand Down Expand Up @@ -193,6 +202,7 @@ const OMP_LAUNCH_SPEC: AcpLaunchSpec = {
},
visualsSkill: loadOmpVisualsSkill,
compaction: true,
transcriptImport: OMP_TRANSCRIPT_IMPORT,
sessionHistory: OMP_SESSION_HISTORY
}

Expand Down
22 changes: 22 additions & 0 deletions src/main/grok/grok-transcript-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { join } from 'node:path'
import { findGrokChatHistoryByCwd, resolveGrokHomeDir } from '../../shared/grok-session-paths'
import { resolveSessionFilePath } from '../native-chat/session-file-resolver'
import type { StructuredAgentTranscriptImport } from '../native-chat/structured-agent-cli-conversations'

export const GROK_TRANSCRIPT_IMPORT: StructuredAgentTranscriptImport = {
// The chat's own home, then the one Session History lists Grok's conversations from.
accountHomeCandidates: ({ selectedAccountHomePath }) => [
selectedAccountHomePath,
resolveGrokHomeDir()
],
findTranscript: async ({ providerSessionId, accountHomePath, workspaceCwd }) => {
const sessionsDir = join(accountHomePath, 'sessions')
return (
(await findGrokChatHistoryByCwd({
sessionId: providerSessionId,
cwd: workspaceCwd,
sessionsDir
})) ?? resolveSessionFilePath('grok', providerSessionId, { grokSessionsDir: sessionsDir })
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,22 @@ function declaringNothing(definition: StructuredAgentDefinition): StructuredAgen
}
}

/** Claude and Codex declaring no capability and no resting options: what a host over a bare
* adapter double declares. */
export const NO_STRUCTURED_AGENTS = new StructuredAgentRegistry([
{ definition: declaringNothing(CLAUDE_STRUCTURED_AGENT), adapter: NO_METHODS },
{ definition: declaringNothing(CODEX_STRUCTURED_AGENT), adapter: NO_METHODS }
/** These agents declaring no capability and no resting options: what a host over a bare adapter
* double declares. */
export function structuredAgentsDeclaringNothing(
definitions: readonly StructuredAgentDefinition[]
): StructuredAgentRegistry {
return new StructuredAgentRegistry(
definitions.map((definition) => ({
definition: declaringNothing(definition),
adapter: NO_METHODS
}))
)
}

export const NO_STRUCTURED_AGENTS = structuredAgentsDeclaringNothing([
CLAUDE_STRUCTURED_AGENT,
CODEX_STRUCTURED_AGENT
])

/** This build's two agents over doubles. Each declares only what its double implements, so a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { StructuredAgentSessionHost } from './structured-agent-session-host'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import { NO_STRUCTURED_AGENTS } from './structured-agent-session-adapter-router-test-support'
import {
NO_STRUCTURED_AGENTS,
structuredAgentsDeclaringNothing
} from './structured-agent-session-adapter-router-test-support'
import { acpStructuredAgentDefinition } from '../../acp/acp-structured-agent-definitions'
import { acpLaunchSpecFor } from '../../acp/acp-launch-specs'

const NOW = 1_800_000_000_000
const SESSION = 'codex_adopting_session'
Expand Down Expand Up @@ -59,7 +64,10 @@ async function writeCodexRollout(path: string, text: string): Promise<void> {
await writeFile(path, `${lines.join('\n')}\n`, 'utf8')
}

function attachParams(transcriptPath?: string): AgentSessionAttachParams {
function attachParams(
transcriptPath?: string,
overrides: Partial<AgentSessionAttachParams> = {}
): AgentSessionAttachParams {
const params: AgentSessionAttachParams = {
envelope: {
sessionId: SESSION,
Expand All @@ -78,9 +86,10 @@ function attachParams(transcriptPath?: string): AgentSessionAttachParams {
accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' },
runtimeKind: 'native',
adopt: {
providerHandle: { kind: 'codex', threadId: THREAD },
providerHandle: codexProviderHandle(THREAD),
...(transcriptPath ? { transcriptPath } : {})
}
},
...overrides
}
return {
...params,
Expand Down Expand Up @@ -121,11 +130,13 @@ function adapter(): StructuredAgentSessionAdapter {
async function attach(
transcriptPath: string | undefined,
sessionAdapter: StructuredAgentSessionAdapter,
onAttached: AttachFlowInput['onAttached'] = () => {}
onAttached: AttachFlowInput['onAttached'] = () => {},
params: AgentSessionAttachParams = attachParams(transcriptPath),
agents = NO_STRUCTURED_AGENTS
) {
store ??= await openTestAgentSessionRecordStore(root!)
return performAttach({
agents: NO_STRUCTURED_AGENTS,
agents,
logger: createStructuredAgentSessionLogger(),
store,
adapter: sessionAdapter,
Expand All @@ -138,7 +149,7 @@ async function attach(
},
callerKey: 'client-1',
optionRevision: () => 0,
params: attachParams(transcriptPath),
params,
now: () => NOW,
onAttached
})
Expand All @@ -160,6 +171,50 @@ describe('adopting a provider conversation on create', () => {
expect(JSON.stringify(page.items)).toContain('ORCA-ADOPT-1')
})

it("seeds an ACP agent's chain with the session it adopts and imports that CLI's history", async () => {
root = await mkdtemp(join(tmpdir(), 'orca-adopt-acp-'))
const transcriptPath = join(root, 'chat_history.jsonl')
await writeFile(
transcriptPath,
`${JSON.stringify({ type: 'user', content: [{ type: 'text', text: 'ORCA-GROK-ADOPT' }] })}\n`
)
const grok = acpLaunchSpecFor('grok')
if (!grok) {
throw new Error('this build registers no Grok chats')
}
const handle = { transport: 'acp', agent: 'grok', nativeId: THREAD }
const sessionAdapter = adapter()
vi.mocked(sessionAdapter.acquire).mockImplementation(async ({ fence, spawnToken }) => ({
process: { hostId: 'local', pid: 4242, processStartTimeMs: NOW, spawnToken },
link: {
linkId: 'resumed-link',
handle,
origin: 'resumed',
mintedAtFence: fence,
observedAt: NOW
}
}))

const result = await attach(
transcriptPath,
sessionAdapter,
() => {},
attachParams(transcriptPath, {
provider: 'grok',
agent: 'grok',
accountHome: { variable: 'GROK_HOME', path: '/home/dev/.grok' },
adopt: { providerHandle: handle, transcriptPath }
}),
structuredAgentsDeclaringNothing([acpStructuredAgentDefinition(grok)])
)

expect(result.ok && JSON.stringify(result.value.page.items)).toContain('ORCA-GROK-ADOPT')
expect(store?.getRecord(SESSION)?.providerHandleChain[0]).toMatchObject({
origin: 'adopted',
handle
})
})

it('replays create without replacing journal-only messages or rereading the source', async () => {
root = await mkdtemp(join(tmpdir(), 'orca-adopt-replay-'))
const transcriptPath = join(root, 'rollout.jsonl')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type AgentSessionWireRefusal
} from '../../../shared/agent-session-wire'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import { agentSessionProviderHandleFromWire } from '../../../shared/agent-session-provider-handle-encoding'
import type { AgentSessionAttachParams, AttachedJournal } from './structured-agent-session-attach'
import type { JournalReplacementItem } from '../agent-session-journal/journal-epoch-replacement'
import {
Expand Down Expand Up @@ -50,7 +49,7 @@ async function readAdoptedTranscript(
}
const prepared = await prepareLegacyTranscriptImport({
agent: params.agent,
sessionId: agentSessionProviderHandleFromWire(adopt.providerHandle).nativeId,
sessionId: adopt.providerHandle.nativeId,
options: { filePath: adopt.transcriptPath }
})
if (!prepared.ok) {
Expand Down Expand Up @@ -110,7 +109,7 @@ async function applyAdoptedTranscript(
const imported = await importLegacyTranscriptIntoJournal({
journal: attached.journal,
agent: params.agent,
sessionId: agentSessionProviderHandleFromWire(adopt.providerHandle).nativeId,
sessionId: adopt.providerHandle.nativeId,
fence: record.lease.runtimeFence,
options: { filePath: adopt.transcriptPath }
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-
import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication'
import type {
StructuredAgentId,
AgentSessionProviderHandle,
AgentSessionProviderHandleLink
} from '../../../shared/agent-session-provider-handle'
import {
agentSessionProviderHandleBelongsTo,
agentSessionProviderHandleFromWire,
type AgentSessionWireProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
import { claudeProviderHandleLink } from '../../claude/claude-structured-owner-identity'
import { codexProviderHandleLink } from '../../codex/codex-structured-owner-identity'
import type {
AgentSessionAccountHome,
AgentSessionExecutionLocation,
Expand Down Expand Up @@ -79,7 +78,7 @@ export type AgentSessionAttachParams = {
* without adopting — presence of a handle must never be what triggers a resume.
*/
adopt?: {
providerHandle: AgentSessionWireProviderHandle
providerHandle: AgentSessionProviderHandle
/** Omitted only when the exact committed operation replays an already-imported journal. */
transcriptPath?: string
}
Expand Down Expand Up @@ -282,25 +281,16 @@ async function reconcileAgainstProviderHistory(input: {
const ADOPTED_HANDLE_FENCE = 1

function adoptedProviderHandleLink(
handle: AgentSessionWireProviderHandle,
handle: AgentSessionProviderHandle,
observedAt: number
): AgentSessionProviderHandleLink {
return handle.kind === 'claude'
? claudeProviderHandleLink({
sessionId: handle.sessionId,
leafUuid: handle.leafUuid,
resumed: false,
origin: 'adopted',
fence: ADOPTED_HANDLE_FENCE,
observedAt
})
: codexProviderHandleLink({
threadId: handle.threadId,
resumed: false,
origin: 'adopted',
fence: ADOPTED_HANDLE_FENCE,
observedAt
})
return {
linkId: `${handle.agent}-${ADOPTED_HANDLE_FENCE}-${handle.nativeId}`.slice(0, 128),
handle,
origin: 'adopted',
mintedAtFence: ADOPTED_HANDLE_FENCE,
observedAt
}
}

export function reserveRequestFor(input: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import type { AgentSessionAttachParams } from './structured-agent-session-attach'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'

let rig: RestTestRig
beforeEach(async () => {
Expand Down Expand Up @@ -47,7 +48,7 @@ async function adoptedParams(history = 'adopted history', threadId = THREAD) {
].join('\n')
)
return createTestParams(undefined, {
adopt: { providerHandle: { kind: 'codex', threadId }, transcriptPath }
adopt: { providerHandle: codexProviderHandle(threadId), transcriptPath }
})
}

Expand Down Expand Up @@ -88,7 +89,7 @@ it('imports adoption at rest and resumes its original provider identity after it
await rig.host.create(CALLER, {
...params,
accountHome: { variable: 'CODEX_HOME', path: '/drifted/account' },
adopt: { providerHandle: { kind: 'codex', threadId: THREAD } }
adopt: { providerHandle: codexProviderHandle(THREAD) }
})
).toMatchObject({ ok: true, replayed: true })
expect(await rig.host.send(CALLER, restTestSend('new work'))).toMatchObject({ ok: true })
Expand Down
Loading
Loading