Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f86ab83
feat(orchestration): inject the Orca session id into structured child…
brennanb2025 Sep 23, 2026
370402d
test(orchestration): pin session id injection for native Claude, nati…
brennanb2025 Sep 23, 2026
720fd27
test(orchestration): pin one caller precedence rule across every CLI …
brennanb2025 Sep 23, 2026
8224b04
refactor(orchestration): keep the identity-less marker reader to the …
brennanb2025 Sep 23, 2026
992f3b1
test(orchestration): pin that a host refusal of the session surfaces …
brennanb2025 Sep 23, 2026
d1672af
fix(orchestration): keep the identity-less marker beside the id for C…
brennanb2025 Sep 23, 2026
2156bea
fix(orchestration): refuse a conflicting --from on gate-list and task…
brennanb2025 Sep 23, 2026
9532502
fix(orchestration): name this app's CLI by absolute path for a struct…
brennanb2025 Sep 24, 2026
0842014
test(orchestration): pin a structured worker's CLI command as this ap…
brennanb2025 Sep 24, 2026
29c279e
test(orchestration): run the zsh login-shell arm in the real-shell la…
brennanb2025 Sep 24, 2026
5ba192e
fix(orchestration): omit a structured child's CLI command when no lau…
brennanb2025 Sep 24, 2026
9c8f180
fix(terminal): name this app's CLI launcher by absolute path in every…
brennanb2025 Sep 24, 2026
92bf593
feat(cli): hand a command to the session's own CLI when another Orca …
brennanb2025 Sep 24, 2026
e9bd636
refactor(orchestration): declare which flag names the caller on each …
brennanb2025 Sep 24, 2026
b200aae
perf(cli): keep the session caller check off the actor codec's module…
brennanb2025 Sep 24, 2026
d49294b
refactor(cli): spell a session's address from the one prefix constant…
brennanb2025 Sep 25, 2026
f0c7886
refactor(orchestration): drop the session id's terminal-view spawn no…
brennanb2025 Sep 25, 2026
72eb439
fix(terminal): run the Codex launch preflight through the CLI the ter…
brennanb2025 Sep 26, 2026
09f1858
revert(terminal): keep terminals on main's ORCA_CLI_COMMAND and Codex…
brennanb2025 Sep 27, 2026
bf4f095
fix(cli): hand off to the session's CLI only inside a structured session
brennanb2025 Sep 27, 2026
8cbf433
fix(cli): name the packaged Windows command after the handoff decision
brennanb2025 Sep 27, 2026
62e9b93
refactor(cli): decide the session handoff from the CLI's own entry, n…
brennanb2025 Sep 27, 2026
d6753dd
refactor(cli): drop the session CLI handoff; the pinned instance and …
brennanb2025 Sep 27, 2026
ccb4b95
test(orchestration): say why the registered worker case pins the hand…
brennanb2025 Sep 27, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ jobs:
src/main/zsh-scoped-histfile.live-shell.test.ts \
src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \
src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts \
src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
src/shared/fish-query-reply-child-stdin.node-pty.test.ts \
src/shared/pty-reply-echo-shapes.node-pty.test.ts \
Expand Down
1 change: 1 addition & 0 deletions config/scripts/ci-unit-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const UNIT_EXCLUDE = [
'src/main/zsh-scoped-histfile.live-shell.test.ts',
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
'src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts',
'src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts',
'src/shared/fish-query-reply-child-stdin.node-pty.test.ts',
'src/shared/pty-reply-echo-shapes.node-pty.test.ts',
Expand Down
3 changes: 2 additions & 1 deletion config/scripts/pr-workflow-parallelism.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const shellContractFiles = [
'src/main/zsh-scoped-histfile.live-shell.test.ts',
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
'src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts',
'src/shared/posix-command-path-lookup.test.ts'
]
const patchedNodePtyContractFiles = [
Expand All @@ -46,7 +47,7 @@ const testFilePatterns = [
// rather than calling spawnSync('zsh') themselves. Without this branch the rule
// silently stops noticing the very tests that need the lane's zsh install.
const realZshUsage =
/(?:spawnSync|execFileSync|spawn)\(\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|spawnSync\(\s*['"]which['"]\s*,\s*\[\s*['"]zsh['"]|name:\s*['"]zsh['"]\s*,\s*path:\s*executablePath|from '[^']*zsh-startup-hook-pty-harness'/
/(?:spawnSync|execFileSync|spawn)\(\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|program:\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|spawnSync\(\s*['"]which['"]\s*,\s*\[\s*['"]zsh['"]|name:\s*['"]zsh['"]\s*,\s*path:\s*executablePath|from '[^']*zsh-startup-hook-pty-harness'/

describe('PR workflow parallelism', () => {
it('keeps lightweight orchestration jobs on the free slim runner', () => {
Expand Down
8 changes: 8 additions & 0 deletions config/scripts/vitest-caller-identity-env-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Why: a structured chat exports its own orchestration caller identity to every child, including a
* test runner it launches. Inherited, it would decide which CLI identity branch a test exercises
* depending on who ran the suite; suites that need one set it themselves.
*/
for (const name of ['ORCA_AGENT_SESSION_ID', 'ORCA_STRUCTURED_SESSION']) {
delete process.env[name]
}
1 change: 1 addition & 0 deletions config/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"../electron.vite.config.*",
"./build-plugins/**/*",
"./scripts/vitest-host-ports-setup.ts",
"./scripts/vitest-caller-identity-env-setup.ts",
"../src/main/**/*",
"../src/renderer/src/lib/skill-freshness-display-status.ts",
"../src/renderer/src/components/native-chat/native-chat-resolution-receipt.ts",
Expand Down
3 changes: 2 additions & 1 deletion config/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export default defineConfig({
setupFiles: [
resolve('config/scripts/happy-dom-offscreen-canvas.ts'),
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
resolve('config/scripts/vitest-host-ports-setup.ts')
resolve('config/scripts/vitest-host-ports-setup.ts'),
resolve('config/scripts/vitest-caller-identity-env-setup.ts')
],
include: UNIT_INCLUDE,
...(process.env.ORCA_BALANCE_UNIT_SHARDS === '1' ? { exclude: UNIT_EXCLUDE } : {}),
Expand Down
5 changes: 5 additions & 0 deletions src/cli/command-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ export type CommandSpec = {
positionalArgs?: string[]
examples?: string[]
notes?: string[]
// Why: `--from`/`--terminal` names either the acting caller or a target, and only the spec can
// say which. An agent session refuses a caller flag naming anyone else before dispatch.
identityFlagRoles?: Partial<Record<IdentityFlag, 'caller' | 'target'>>
}

export type IdentityFlag = 'from' | 'terminal'

export function specPaths(spec: CommandSpec): string[][] {
return spec.aliases ? [spec.path, ...spec.aliases] : [spec.path]
}
8 changes: 6 additions & 2 deletions src/cli/handlers/orchestration/dispatch-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { orchestrationMigrationData } from '../../../shared/orchestration-rpc-co
import { callOrchestrationMutation } from './mutation-request'
import { isDevCliInvocation } from './runtime-compatibility'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { injectedSessionAddress } from '../../../shared/agent-session-caller-env'

export const ORCHESTRATION_DISPATCH_HANDLER: Record<string, CommandHandler> = {
'orchestration dispatch': async ({ flags, client, cwd, json }) => {
Expand Down Expand Up @@ -42,9 +43,12 @@ export const ORCHESTRATION_DISPATCH_HANDLER: Record<string, CommandHandler> = {
export const ORCHESTRATION_DISPATCH_INSPECTION_HANDLERS: Record<string, CommandHandler> = {
'orchestration dispatch-show': async ({ flags, client, cwd, json }) => {
const showPreamble = flags.has('preamble') ? true : undefined
// Why: a preview must embed the same real coordinator handle as an actual dispatch.
// Why: a preview must embed the same real coordinator handle as an actual dispatch. Its --from
// only fills preview text and names no caller, so it passes through unfenced.
const from = showPreamble
? await resolveCoordinatorTerminalHandle(flags, cwd, client)
? (getOptionalStringFlag(flags, 'from') ??
injectedSessionAddress() ??
(await resolveCoordinatorTerminalHandle(flags, cwd, client)))
: undefined
const result = await client.call<{
dispatch: { id: string; task_id: string; status: string } | null
Expand Down
12 changes: 8 additions & 4 deletions src/cli/handlers/orchestration/message-check-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { startCheckKeepalive } from './check-keepalive'
import { callOrchestrationMutation } from './mutation-request'
import { getOptionalPositiveIntegerValueFlag } from './numeric-flags'
import { flushOrchestrationStdout, resolveCompatibilityCliCommand } from './runtime-compatibility'
import { resolveOrchestrationTerminalHandle } from './terminal-identity'
import { orchestrationCallerLabel, resolveOrchestrationTerminalHandle } from './terminal-identity'

type CheckResult = {
messages: MessageSummary[]
Expand Down Expand Up @@ -41,12 +41,16 @@ export const ORCHESTRATION_CHECK_HANDLER: Record<string, CommandHandler> = {
const timeoutMs = getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms')
const explicitTerminal = getOptionalStringFlag(flags, 'terminal')
const terminal = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'terminal')
// Why: a session names itself by its id alone; a pane key it inherited is not its identity.
const paneKey =
explicitTerminal || terminal === undefined ? undefined : process.env.ORCA_PANE_KEY
const callerLabel = orchestrationCallerLabel(terminal)
const stopKeepalive = wait ? startCheckKeepalive(timeoutMs) : null
let result: Awaited<ReturnType<typeof client.call<CheckResult>>>
try {
result = await callOrchestrationMutation<CheckResult>(client, flags, 'orchestration.check', {
terminal,
terminalPaneKey: explicitTerminal ? undefined : process.env.ORCA_PANE_KEY || undefined,
terminalPaneKey: paneKey || undefined,
// Why: old runtimes degrade peek to non-consuming all mode instead of destructive mark-read.
unread: flags.has('unread') ? true : peek ? false : undefined,
peek: peek ? true : undefined,
Expand All @@ -68,9 +72,9 @@ export const ORCHESTRATION_CHECK_HANDLER: Record<string, CommandHandler> = {
}
result = {
...result,
result: prepareOrchestrationCheckOutput(result.result, terminal, flags.has('format'))
result: prepareOrchestrationCheckOutput(result.result, callerLabel, flags.has('format'))
}
printResult(result, json, (value) => formatOrchestrationCheckText(value, terminal))
printResult(result, json, (value) => formatOrchestrationCheckText(value, callerLabel))
const compatibilityAck = result.result.legacyCompatibility?.ackMessageIds
if (compatibilityAck && compatibilityAck.length > 0) {
await flushOrchestrationStdout()
Expand Down
7 changes: 5 additions & 2 deletions src/cli/handlers/orchestration/message-send-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CommandHandler } from '../../dispatch'
import { printResult } from '../../format'
import { getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { readInjectedAgentSessionId } from '../../../shared/agent-session-caller-env'
import { requireWorkerDoneSettlement } from '../orchestration-worker-settlement'
import { getOptionalStructuredMessagePayload } from './message-payload'
import { callOrchestrationMutation } from './mutation-request'
Expand Down Expand Up @@ -75,7 +76,8 @@ export const ORCHESTRATION_SEND_HANDLER: Record<string, CommandHandler> = {
if (
(type === 'worker_done' || type === 'heartbeat') &&
!getOptionalStringFlag(flags, 'from') &&
!process.env.ORCA_TERMINAL_HANDLE
!process.env.ORCA_TERMINAL_HANDLE &&
!readInjectedAgentSessionId()
) {
// Why: focus isn't lifecycle authority — an identity-less subprocess must fail closed rather than guess the worker.
throwNoActiveSenderTerminal()
Expand All @@ -94,7 +96,8 @@ export const ORCHESTRATION_SEND_HANDLER: Record<string, CommandHandler> = {
threadId: getOptionalStringFlag(flags, 'thread-id'),
payload: getOptionalStructuredMessagePayload(flags),
// Why: pane key is the remint-stable sender identity the runtime verifies lifecycle ownership against; older runtimes strip it.
senderPaneKey: process.env.ORCA_PANE_KEY || undefined,
// A session names itself by its id alone.
senderPaneKey: from === undefined ? undefined : process.env.ORCA_PANE_KEY || undefined,
waitForLifecycleSettlement: type === 'worker_done' ? true : undefined,
devMode: isDevCliInvocation()
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/handlers/orchestration/question-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
resolveOrchestrationCliExecutable(),
'orchestration',
'ask',
'--from',
from,
// A session's resume is flagless: its injected id names it again.
...(from ? ['--from', from] : []),
...(dispatchCapability ? ['--dispatch-capability', dispatchCapability] : []),
'--resume',
messageId,
Expand Down
4 changes: 3 additions & 1 deletion src/cli/handlers/orchestration/run-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export const ORCHESTRATION_RUN_HANDLERS: Record<string, CommandHandler> = {
run: { id: string; objective: string } | null
}>('orchestration.runCurrent', { from })
printResult(result, json, (r) =>
r.run ? `${r.run.id} ${r.run.objective}` : 'No Run is bound to this terminal.'
r.run
? `${r.run.id} ${r.run.objective}`
: `No Run is bound to this ${from === undefined ? 'session' : 'terminal'}.`
)
},

Expand Down
28 changes: 23 additions & 5 deletions src/cli/handlers/orchestration/terminal-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@ import type { RuntimeClient } from '../../runtime-client'
import { getOptionalStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { getTerminalHandle } from '../../selectors'
import { isStructuredSessionWithoutIdentity } from '../../../shared/structured-session-marker'
import { hasStructuredSessionMarker } from '../../../shared/structured-session-marker'
import {
injectedSessionAddress,
readInjectedAgentSessionId
} from '../../../shared/agent-session-caller-env'

/**
* The caller's terminal handle, or `undefined` when an injected agent session id names the caller:
* the orchestration envelope carries that id and the host binds the caller param to it, so nothing
* is resolved or guessed here.
*/
export async function resolveOrchestrationTerminalHandle(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient,
flagName: 'from' | 'terminal',
options: { validateEnvHandle?: boolean } = {}
): Promise<string> {
): Promise<string | undefined> {
// A caller flag naming anyone else was already refused at the CLI entry, from the command's spec.
if (readInjectedAgentSessionId()) {
return undefined
}
const explicit = getOptionalStringFlag(flags, flagName)
if (explicit) {
return explicit
Expand All @@ -35,7 +48,7 @@ export async function resolveOrchestrationTerminalHandle(
// default, so that guess consumed another pane's oldest unread batch and marked it read, and the
// rightful worker never saw its mail. Refusing is the only honest answer: this child genuinely
// cannot infer its own identity.
if (isStructuredSessionWithoutIdentity()) {
if (hasStructuredSessionMarker()) {
throw structuredSessionRefusal(flagName)
}
if (flagName === 'from') {
Expand Down Expand Up @@ -157,11 +170,16 @@ function getClientErrorMessage(err: unknown): string | undefined {
return typeof message === 'string' ? message : undefined
}

/** How check output names its caller: the handle, or the session's address. */
export function orchestrationCallerLabel(handle: string | undefined): string {
return handle ?? injectedSessionAddress() ?? 'unknown'
}

export async function resolveCoordinatorTerminalHandle(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<string> {
): Promise<string | undefined> {
return await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from', {
validateEnvHandle: true
})
Expand Down Expand Up @@ -204,7 +222,7 @@ export function throwNoActiveSenderTerminal(): never {
// place left that would tell an identity-less session to pass a handle it does not have. A stale
// ORCA_TERMINAL_HANDLE is a different case — that caller HAS an identity, so it keeps the advice
// to re-run under a live one.
if (isStructuredSessionWithoutIdentity() && !process.env.ORCA_TERMINAL_HANDLE) {
if (hasStructuredSessionMarker() && !process.env.ORCA_TERMINAL_HANDLE) {
throw structuredSessionRefusal('from')
}
throw new RuntimeClientError(
Expand Down
19 changes: 19 additions & 0 deletions src/cli/index-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ describe('orca cli worktree awareness', () => {
expect(logSpy).toHaveBeenCalledWith('Sent 2 messages to 2 recipients')
})

it("refuses an agent session's caller flag naming another caller before any request", async () => {
// One chokepoint for every verb: the spec says which flag names the caller.
process.env.ORCA_AGENT_SESSION_ID = 'f7a1c0de-1111-4222-8333-444455556666'
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
try {
await main(['orchestration', 'check', '--terminal', 'term_sibling', '--json'], '/tmp/repo')
} finally {
delete process.env.ORCA_AGENT_SESSION_ID
}

expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: { code: 'consumer_fenced' }
})
process.exitCode = undefined
})

it('rejects no-flag orchestration reset before calling the runtime', async () => {
await main(['orchestration', 'reset'], '/tmp/repo')

Expand Down
5 changes: 5 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { printHelp } from './help'
import type { RuntimeClient } from './runtime-client'
import { COMMAND_SPECS } from './specs'
import { resolveOrchestrationCliExecutable } from './runtime/orchestration-recovery-command'
import { refuseConflictingSessionCallerFlags } from './session-caller-flags'

export { COMMAND_SPECS } from './specs'
export { buildCurrentWorktreeSelector, normalizeWorktreeSelector } from './selectors'
Expand Down Expand Up @@ -111,6 +112,10 @@ export async function main(
// lookup so users do not get misleading "Orca is not running" failures for
// simple command typos or unsupported flags.
validateCommandAndFlags(COMMAND_SPECS, parsed)
refuseConflictingSessionCallerFlags(
findCommandSpec(COMMAND_SPECS, parsed.commandPath),
parsed.flags
)
const RuntimeClientClass = await loadRuntimeClientClass()
const ignoreRemoteSelection = shouldIgnoreRemoteSelection(parsed.commandPath)
const pairingCode = ignoreRemoteSelection ? null : parsed.flags.get('pairing-code')
Expand Down
Loading
Loading