Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5d374e9
Add agent session identity to orchestration runs
Sep 8, 2026
1e6f94d
Add agent session identity columns to orchestration records
Sep 8, 2026
7817970
Fix run creation values for agent session identity
Sep 8, 2026
f80202b
Resolve run mailbox owners by agent session id
Sep 8, 2026
d4ee902
fix orchestration session identity column projections
Sep 8, 2026
a0cfed3
Persist dispatch assignee agent session identity
Sep 8, 2026
bb73d36
fix dispatch session identity propagation
Sep 8, 2026
7062cfa
fix optional legacy orchestration session columns
Sep 8, 2026
5e3df53
Expose agent session identity from orchestration environment
Sep 8, 2026
4315204
allow native session runs without terminal projections
Sep 8, 2026
ebd6360
allow native session coordinators to create runs
Sep 8, 2026
281e4a1
Revert "allow native session coordinators to create runs"
Sep 8, 2026
b79c81c
authenticate native session run creation
Sep 8, 2026
e724bea
fix native run authority type narrowing
Sep 8, 2026
f3e1118
support authenticated native run rebinding
Sep 8, 2026
1680534
resolve native current runs by session identity
Sep 8, 2026
eafcb48
route orchestration scope by authenticated agent session
Sep 8, 2026
cc3a18c
propagate native session identity through CLI orchestration
Sep 8, 2026
98069cb
support native session identity for orchestration send
Sep 8, 2026
429011f
fence native current run lookup
Sep 8, 2026
9c23b61
propagate native session identity to structured children
Sep 8, 2026
301ad25
support native session worker start
Sep 8, 2026
4027bf5
resolve native worker launch through session identity
Sep 8, 2026
3a24b10
validate durable native session authority
Sep 8, 2026
9d3e85a
route native orchestration mailbox checks by session
Sep 8, 2026
2ab2405
format native orchestration question handler
Sep 8, 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
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions src/cli/handlers/orchestration/dispatch-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { RuntimeClientError } from '../../runtime-client'
import { orchestrationMigrationData } from '../../../shared/orchestration-rpc-contract'
import { callOrchestrationMutation } from './mutation-request'
import { isDevCliInvocation } from './runtime-compatibility'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { orchestrationSessionPayload, resolveCoordinatorTerminalHandle } from './terminal-identity'

export const ORCHESTRATION_DISPATCH_HANDLER: Record<string, CommandHandler> = {
'orchestration dispatch': async ({ flags, client, cwd, json }) => {
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
const session = flags.has('from') ? {} : orchestrationSessionPayload()
const from = session.agentSessionId
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const dryRun = flags.has('dry-run') ? true : undefined
const returnPreamble = flags.has('return-preamble') ? true : undefined
// Why: --to is only required for non-dry-run; the RPC handler re-enforces.
Expand All @@ -23,6 +26,7 @@ export const ORCHESTRATION_DISPATCH_HANDLER: Record<string, CommandHandler> = {
task: getRequiredStringFlag(flags, 'task'),
run: getOptionalStringFlag(flags, 'run'),
to,
...session,
from,
inject: flags.has('inject') ? true : undefined,
dryRun,
Expand All @@ -43,7 +47,8 @@ export const ORCHESTRATION_DISPATCH_INSPECTION_HANDLERS: Record<string, CommandH
'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.
const from = showPreamble
const session = flags.has('from') ? {} : orchestrationSessionPayload()
const from = showPreamble && !session.agentSessionId
? await resolveCoordinatorTerminalHandle(flags, cwd, client)
: undefined
const result = await client.call<{
Expand All @@ -52,6 +57,7 @@ export const ORCHESTRATION_DISPATCH_INSPECTION_HANDLERS: Record<string, CommandH
}>('orchestration.dispatchShow', {
task: getRequiredStringFlag(flags, 'task'),
preamble: showPreamble,
...session,
from,
devMode: isDevCliInvocation()
})
Expand Down
19 changes: 14 additions & 5 deletions src/cli/handlers/orchestration/gate-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CommandHandler } from '../../dispatch'
import { printResult } from '../../format'
import { getOptionalJsonFlag, getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { callOrchestrationMutation } from './mutation-request'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { orchestrationSessionPayload, resolveCoordinatorTerminalHandle } from './terminal-identity'

export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
'orchestration gate-create': async ({ flags, client, cwd, json }) => {
Expand All @@ -13,7 +13,9 @@ export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
question: getRequiredStringFlag(flags, 'question'),
options: getOptionalJsonFlag(flags, 'options'),
// Why: gates are Run-scoped, so the coordinator handle is the authorized caller identity.
from: await resolveCoordinatorTerminalHandle(flags, cwd, client)
...(orchestrationSessionPayload().agentSessionId
? orchestrationSessionPayload()
: { from: await resolveCoordinatorTerminalHandle(flags, cwd, client) })
})
printResult(
result,
Expand All @@ -29,15 +31,21 @@ export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
}>(client, flags, 'orchestration.gateResolve', {
id: getRequiredStringFlag(flags, 'id'),
resolution: getRequiredStringFlag(flags, 'resolution'),
from: await resolveCoordinatorTerminalHandle(flags, cwd, client)
...(orchestrationSessionPayload().agentSessionId
? orchestrationSessionPayload()
: { from: await resolveCoordinatorTerminalHandle(flags, cwd, client) })
})
printResult(result, json, (value) => `Gate ${value.gate.id} resolved: ${value.gate.resolution}`)
},

'orchestration gate-list': async ({ flags, client, cwd, json }) => {
const run = getOptionalStringFlag(flags, 'run')
// Why: named runs remain inspectable without a pane; only implicit runs resolve identity.
const from = run ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const session = orchestrationSessionPayload()
const from =
run || session.agentSessionId
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
gates: { id: string; task_id: string; question: string; status: string }[]
count: number
Expand All @@ -46,7 +54,8 @@ export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
task: getOptionalStringFlag(flags, 'task'),
status: getOptionalStringFlag(flags, 'status'),
run,
from
from,
...session
})
printResult(result, json, (value) => {
if (value.gates.length === 0) {
Expand Down
16 changes: 11 additions & 5 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 { orchestrationSessionPayload, resolveOrchestrationTerminalHandle } from './terminal-identity'

type CheckResult = {
messages: MessageSummary[]
Expand All @@ -40,13 +40,18 @@ 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')
const session = explicitTerminal ? {} : orchestrationSessionPayload()
const terminal = session.agentSessionId
? undefined
: await resolveOrchestrationTerminalHandle(flags, cwd, client, 'terminal')
const checkedIdentity = terminal ?? session.agentSessionId!
const stopKeepalive = wait ? startCheckKeepalive(timeoutMs) : null
let result: Awaited<ReturnType<typeof client.call<CheckResult>>>
try {
result = await callOrchestrationMutation<CheckResult>(client, flags, 'orchestration.check', {
...session,
terminal,
terminalPaneKey: explicitTerminal ? undefined : process.env.ORCA_PANE_KEY || undefined,
terminalPaneKey: explicitTerminal || session.agentSessionId ? undefined : process.env.ORCA_PANE_KEY || 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,13 +73,14 @@ export const ORCHESTRATION_CHECK_HANDLER: Record<string, CommandHandler> = {
}
result = {
...result,
result: prepareOrchestrationCheckOutput(result.result, terminal, flags.has('format'))
result: prepareOrchestrationCheckOutput(result.result, checkedIdentity, flags.has('format'))
}
printResult(result, json, (value) => formatOrchestrationCheckText(value, terminal))
printResult(result, json, (value) => formatOrchestrationCheckText(value, checkedIdentity))
const compatibilityAck = result.result.legacyCompatibility?.ackMessageIds
if (compatibilityAck && compatibilityAck.length > 0) {
await flushOrchestrationStdout()
await client.call('orchestration.check', {
...session,
terminal,
compatibilityAck: JSON.stringify({
messageIds: compatibilityAck,
Expand Down
10 changes: 8 additions & 2 deletions src/cli/handlers/orchestration/message-send-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getOptionalStructuredMessagePayload } from './message-payload'
import { callOrchestrationMutation } from './mutation-request'
import { isDevCliInvocation } from './runtime-compatibility'
import {
orchestrationSessionPayload,
resolveOrchestrationTerminalHandle,
throwNoActiveSenderTerminal
} from './terminal-identity'
Expand Down Expand Up @@ -75,16 +76,21 @@ 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 &&
!orchestrationSessionPayload().agentSessionId
) {
// Why: focus isn't lifecycle authority — an identity-less subprocess must fail closed rather than guess the worker.
throwNoActiveSenderTerminal()
}

// Why: lifecycle senders preserve ORCA_TERMINAL_HANDLE across restarts for older runtimes.
const from = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
const session = orchestrationSessionPayload()
const from = session.agentSessionId
? undefined
: await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
const sendParams = {
from,
...session,
to,
run: getOptionalStringFlag(flags, 'run'),
subject: getRequiredStringFlag(flags, 'subject'),
Expand Down
15 changes: 11 additions & 4 deletions src/cli/handlers/orchestration/question-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ import {
resolveCompatibilityCliCommand,
resolvePackagedWindowsCompatibilityCommand
} from './runtime-compatibility'
import { resolveOrchestrationTerminalHandle } from './terminal-identity'
import {
orchestrationSessionPayload,
resolveOrchestrationTerminalHandle
} from './terminal-identity'

export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
'orchestration ask': async ({ flags, client, cwd, json }) => {
const parsedTimeoutMs = getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms')
const timeoutMs = clampOrchestrationAskTimeoutMs(parsedTimeoutMs)
const from = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
const session = flags.has('from') ? {} : orchestrationSessionPayload()
const from = session.agentSessionId
? undefined
: await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
const question = getOptionalStringFlag(flags, 'question')
const resume = getOptionalStringFlag(flags, 'resume')
if ((question ? 1 : 0) + (resume ? 1 : 0) !== 1) {
Expand Down Expand Up @@ -58,6 +64,7 @@ export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
resume,
options: getOptionalStringFlag(flags, 'options'),
timeoutMs: parsedTimeoutMs === undefined ? undefined : timeoutMs,
...session,
from,
compatibilityCliCommand: resolveCompatibilityCliCommand(),
compatibilityWindowsCommand: resolvePackagedWindowsCompatibilityCommand()
Expand Down Expand Up @@ -86,6 +93,7 @@ export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
if (answerAck && result.result.answer !== null) {
await flushOrchestrationStdout()
await client.call('orchestration.check', {
...session,
terminal: from,
compatibilityQuestionAck: JSON.stringify(answerAck)
})
Expand All @@ -103,8 +111,7 @@ export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
resolveOrchestrationCliExecutable(),
'orchestration',
'ask',
'--from',
from,
...(from ? ['--from', from] : []),
...(dispatchCapability ? ['--dispatch-capability', dispatchCapability] : []),
'--resume',
messageId,
Expand Down
30 changes: 23 additions & 7 deletions src/cli/handlers/orchestration/run-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,53 @@ import {
} from '../../flags'
import { ORCHESTRATION_RUN_PAGE_LIMIT } from '../../../shared/orchestration-run-pagination'
import { callOrchestrationMutation } from './mutation-request'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import {
resolveCoordinatorTerminalHandle,
resolveOrchestrationAgentSessionId,
resolveOrchestrationRuntimeFence
} from './terminal-identity'

export const ORCHESTRATION_RUN_HANDLERS: Record<string, CommandHandler> = {
'orchestration run-create': async ({ flags, client, cwd, json }) => {
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
const sessionId = resolveOrchestrationAgentSessionId()
const fence = resolveOrchestrationRuntimeFence()
const from = sessionId ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await callOrchestrationMutation<{
run: { id: string; objective: string; consumer_generation: number }
}>(client, flags, 'orchestration.runCreate', {
objective: getRequiredStringFlag(flags, 'objective'),
from
...(from ? { from } : {}),
...(sessionId ? { agentSessionId: sessionId, runtimeFence: Number(fence) } : {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a complete session payload before disabling terminal routing.

If ORCA_AGENT_SESSION_ID is set and ORCA_AGENT_SESSION_RUNTIME_FENCE is absent, these branches omit from and send runtimeFence: Number(undefined), which is NaN. This breaks the terminal compatibility path for an incomplete native environment. Build orchestrationSessionPayload() once, and use its agentSessionId to choose session routing.

  • src/cli/handlers/orchestration/run-handlers.ts#L26-L26: derive the native payload from orchestrationSessionPayload() instead of converting an optional fence.
  • src/cli/handlers/orchestration/run-handlers.ts#L40-L40: apply the same complete-payload check for orchestration.runUse.
  • src/cli/handlers/orchestration/run-handlers.ts#L54-L54: apply the same complete-payload check for orchestration.runCurrent.
📍 Affects 1 file
  • src/cli/handlers/orchestration/run-handlers.ts#L26-L26 (this comment)
  • src/cli/handlers/orchestration/run-handlers.ts#L40-L40
  • src/cli/handlers/orchestration/run-handlers.ts#L54-L54

})
printResult(result, json, (r) => `Run ${r.run.id} created and bound: ${r.run.objective}`)
},

'orchestration run-use': async ({ flags, client, cwd, json }) => {
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
const sessionId = resolveOrchestrationAgentSessionId()
const fence = resolveOrchestrationRuntimeFence()
const from = sessionId ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await callOrchestrationMutation<{
run: { id: string; objective: string; consumer_generation: number }
}>(client, flags, 'orchestration.runUse', {
id: getRequiredStringFlag(flags, 'id'),
from,
...(from ? { from } : {}),
...(sessionId ? { agentSessionId: sessionId, runtimeFence: Number(fence) } : {}),
...(flags.has('takeover-legacy') ? { takeoverLegacy: true } : {})
})
printResult(result, json, (r) => `Using Run ${r.run.id}: ${r.run.objective}`)
},

'orchestration run-current': async ({ flags, client, cwd, json }) => {
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
const sessionId = resolveOrchestrationAgentSessionId()
const from = sessionId ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
run: { id: string; objective: string } | null
}>('orchestration.runCurrent', { from })
}>('orchestration.runCurrent', {
...(from ? { from } : {}),
...(sessionId
? { agentSessionId: sessionId, runtimeFence: Number(resolveOrchestrationRuntimeFence()) }
: {})
})
printResult(result, json, (r) =>
r.run ? `${r.run.id} ${r.run.objective}` : 'No Run is bound to this terminal.'
)
Expand Down
25 changes: 17 additions & 8 deletions src/cli/handlers/orchestration/task-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { abbreviateOrchestrationTasks } from '../../../shared/orchestration-task-summary'
import { callOrchestrationMutation } from './mutation-request'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { orchestrationSessionPayload, resolveCoordinatorTerminalHandle } from './terminal-identity'

const TASK_STATUS_VALUES = [
'pending',
Expand All @@ -17,7 +17,10 @@ const TASK_STATUS_VALUES = [

export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
'orchestration task-create': async ({ flags, client, cwd, json }) => {
const callerTerminalHandle = await resolveCoordinatorTerminalHandle(flags, cwd, client)
const session = orchestrationSessionPayload()
const callerTerminalHandle = session.agentSessionId
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await callOrchestrationMutation<{ task: { id: string; status: string } }>(
client,
flags,
Expand All @@ -29,7 +32,8 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
deps: getOptionalStringFlag(flags, 'deps'),
parent: getOptionalStringFlag(flags, 'parent'),
run: getOptionalStringFlag(flags, 'run'),
callerTerminalHandle
callerTerminalHandle,
...session

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

task-create (and task-list at :70, task-update at :116-118) propagate agentSessionId/runtimeFence, but the RPC handlers orchestration.taskCreate/taskList/taskUpdate (message-methods.ts:152,191,225) still pass only callerTerminalHandle into resolveRunScope and ignore agentSessionId. For a native session callerTerminalHandle is undefined, so these calls fall through to run_required — task scope never resolves through the session lease, contradicting the PR summary's claim that task scope routes by session identity.

}
)
printResult(result, json, (r) => `Created ${r.task.id} [${r.task.status}]`)
Expand All @@ -38,9 +42,11 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
'orchestration task-list': async ({ flags, client, cwd, json }) => {
const brief = flags.has('brief')
const run = getOptionalStringFlag(flags, 'run')
const callerTerminalHandle = run
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const session = orchestrationSessionPayload()
const callerTerminalHandle =
run || session.agentSessionId
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
tasks: {
id: string
Expand All @@ -60,7 +66,8 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
ready: flags.has('ready') ? true : undefined,
brief: brief ? true : undefined,
run,
callerTerminalHandle
callerTerminalHandle,
...session
})
// Why: only older runtimes (no spec_truncated) skip server-side abbreviation and need this client-side fallback.
const needsClientAbbreviation =
Expand Down Expand Up @@ -106,7 +113,9 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
status,
result: getOptionalStringFlag(flags, 'result'),
run: getOptionalStringFlag(flags, 'run'),
callerTerminalHandle: await resolveCoordinatorTerminalHandle(flags, cwd, client)
...(orchestrationSessionPayload().agentSessionId
? orchestrationSessionPayload()
: { callerTerminalHandle: await resolveCoordinatorTerminalHandle(flags, cwd, client) })
}
)
printResult(result, json, (r) => `Updated ${r.task.id} -> ${r.task.status}`)
Expand Down
Loading
Loading