Skip to content
Open
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
e217893
feat(orchestration): deliver worker results to a structured chat coor…
brennanb2025 Sep 23, 2026
3638aa0
test(orchestration): pin coordinator delivery through the real sessio…
brennanb2025 Sep 24, 2026
0727f57
test(orchestration): type the coordinator mail fixture's attach params
brennanb2025 Sep 24, 2026
72b0c73
fix(orchestration): refuse session recipients with the caller codes, …
brennanb2025 Sep 24, 2026
0e26182
fix(orchestration): wait out a pending worker preamble instead of dis…
brennanb2025 Sep 24, 2026
4dd65b3
test(orchestration): pin a chat's terminal view reading the chat's co…
brennanb2025 Sep 24, 2026
ad0f56c
test(orchestration): read coordinator mail fixtures through checked g…
brennanb2025 Sep 24, 2026
cbaf863
fix(orchestration): name a structured session's CLI by $ORCA_CLI_COMM…
brennanb2025 Sep 24, 2026
b1e6054
fix(orchestration): render the pointer's CLI invocation for the shell…
brennanb2025 Sep 24, 2026
e6f0d8e
fix(orchestration): address a session recipient where its check reads…
brennanb2025 Sep 24, 2026
e8d33aa
test(orchestration): pin the runtime's own idle-edge redrive wiring; …
brennanb2025 Sep 24, 2026
d71febe
fix(orchestration): point mail that has not been pointed, not mail no…
brennanb2025 Sep 24, 2026
feb42de
feat(orchestration): hand a /clear-replaced chat's Runs and unread ma…
brennanb2025 Sep 24, 2026
2c1c936
feat(orchestration): adopt a /clear predecessor's Runs at the clear's…
brennanb2025 Sep 24, 2026
2ddbd1b
fix(orchestration): log a wake that could not resume a session, inste…
brennanb2025 Sep 24, 2026
e3f0b36
test(orchestration): give coordinator mail waits a budget that holds …
brennanb2025 Sep 24, 2026
7b4c100
refactor(orchestration): narrow a retained pointer's dispatch state b…
brennanb2025 Sep 24, 2026
e4d53ae
fix(orchestration): give back a pointer whose admitted turn never ran
brennanb2025 Sep 24, 2026
a79ac15
fix(native-chat): keep a committed /clear from failing on its replace…
brennanb2025 Sep 24, 2026
9e31f3a
test(orchestration): pin /clear adoption across a chain of clears and…
brennanb2025 Sep 24, 2026
737ceed
test(orchestration): type the pending-settlement host test's send inp…
brennanb2025 Sep 24, 2026
d7a4fb7
fix(orchestration): keep a chat's orchestration address across /clear…
brennanb2025 Sep 24, 2026
d42487e
fix(orchestration): give back a structured pointer claim an earlier p…
brennanb2025 Sep 24, 2026
bdc92d7
test(orchestration): pin that a cleared chat's sends carry its conver…
brennanb2025 Sep 24, 2026
36d32dd
fix(orchestration): open the orchestration database at an idle edge o…
brennanb2025 Sep 24, 2026
92ecd41
fix(orchestration): read a chat-coordinated Run's session through the…
brennanb2025 Sep 24, 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
15 changes: 13 additions & 2 deletions src/main/runtime/orca-runtime-automation-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../../shared/automation-list-scope'
import { OrchestrationDb } from './orchestration/db'
import { join } from 'node:path'
import { existsSync } from 'node:fs'
import { getAppEnvironment } from '../../shared/app-environment'
import type { LegacyWorkerTerminalRecoveryPlan } from './orchestration/orchestration-legacy-worker-terminal-recovery'
import type { LegacyWorkerTerminalRecoveryResult } from './runtime-legacy-worker-terminal-recovery-types'
Expand Down Expand Up @@ -151,14 +152,24 @@ export class OrcaRuntimeWithAutomationOperations extends OrcaRuntimeWithPtyForeg
// to inject an in-memory DB without touching the filesystem.
getOrchestrationDb(): OrchestrationDb {
if (!this._orchestrationDb) {
const dbPath = join(getAppEnvironment().getPath('userData'), 'orchestration.db')
this._orchestrationDb = new OrchestrationDb(dbPath)
this._orchestrationDb = new OrchestrationDb(this.orchestrationDbPath())
this.ensureOrchestrationFederationRelay()
this.scheduleRestoredMessageRepoints()
}
return this._orchestrationDb
}

/** The database, opened only if it already exists: a profile without one has no mail to redrive. */
getExistingOrchestrationDb(): OrchestrationDb | null {
return this._orchestrationDb || existsSync(this.orchestrationDbPath())
? this.getOrchestrationDb()
: null
}

private orchestrationDbPath(): string {
return join(getAppEnvironment().getPath('userData'), 'orchestration.db')
}

setOrchestrationDb(db: OrchestrationDb): void {
this.orchestrationFederation.resetForDatabaseChange()
this.mailPointerRepointScheduler.clear()
Expand Down
52 changes: 50 additions & 2 deletions src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { resolveStructuredWorkerAuthority } from './structured-worker-authority'
import { structuredWorkerIdentities } from './structured-worker-identity'
import { isSettledNativeOwner } from './orchestration/structured-session-pointer-delivery'
import type { StructuredPointerTarget } from './orchestration/structured-mailbox-pointer-delivery'
import { releaseRestoredStructuredPointerClaims } from './orchestration/structured-pointer-claim-restore'
import {
handleLessCoordinatorSessionId,
findConnectedPtyBoundToSession,
structuredSessionAddressTarget,
structuredSessionMailDestination,
structuredSessionMailTarget,
structuredSessionIdleEdgeMailboxes
} from './orchestration/structured-session-mail-target'
import {
resolveTerminalIdentityFromProbes,
type RuntimeTerminalIdentity
Expand Down Expand Up @@ -189,6 +198,34 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM
this.orchestrationStructuredMailboxPointerDelivery.onJournalActivity(sessionId)
}

/**
* Every structured session's status change reaches here. At its idle edge, retry what is parked
* on it and re-derive the mailboxes it owns, so mail it could not take earlier (evicted, closed,
* in its terminal view) is pointed again. Workers and chats alike: this is not per-dispatch.
*/
onStructuredSessionStatusForMail(summary: {
sessionId: string
status: 'working' | 'attention' | 'idle' | null
}): void {
if (summary.status === 'working' || summary.status === 'attention') {
return
}
this.notifyStructuredSessionJournalActivity(summary.sessionId)
const openDb = () => this.getExistingOrchestrationDb()
const deliver = (mailbox: string) => this.deliverPendingMessagesForHandle(mailbox)
structuredSessionIdleEdgeMailboxes(summary.sessionId, openDb).forEach(deliver)
}

/** The terminal of a session's terminal view, while a TUI owns it; the PTY lane types there. */
getTerminalViewHandleForSession(sessionId: string): string | null {
const destination = structuredSessionMailDestination(sessionId, this._orchestrationDb)
const pty =
destination?.view === 'terminal-view'
? findConnectedPtyBoundToSession(this.ptysById.values(), destination.sessionId)
: undefined
return pty?.paneKey ? this.getTerminalHandleForPaneKey(pty.paneKey) : null
}

/** Settlement drops anything parked for the session; nothing will ever redrive it again. */
forgetStructuredSessionMail(sessionId: string): void {
this.orchestrationStructuredMailboxPointerDelivery.forgetSession(sessionId)
Expand All @@ -207,6 +244,10 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM
if (mailboxHandle.startsWith('run:')) {
return this.resolveStructuredCoordinatorMailboxTarget(mailboxHandle.slice('run:'.length))
}
const addressed = structuredSessionAddressTarget(mailboxHandle, this._orchestrationDb)
if (addressed !== undefined) {
return addressed
}
if (!mailboxHandle.startsWith('dispatch:')) {
return this.resolveStructuredWorkerDirectMailboxTarget(mailboxHandle)
}
Expand All @@ -233,7 +274,12 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM
protected resolveStructuredCoordinatorMailboxTarget(
runId: string
): StructuredPointerTarget | null {
const coordinator = this._orchestrationDb?.getRun?.(runId)?.coordinator_handle
const run = this._orchestrationDb?.getRun?.(runId)
const sessionId = run ? handleLessCoordinatorSessionId(run) : null
if (sessionId) {
return structuredSessionMailTarget(sessionId, this._orchestrationDb)
}
const coordinator = run?.coordinator_handle
if (!coordinator) {
return null
}
Expand Down Expand Up @@ -295,9 +341,11 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM
}

protected scheduleRestoredMessageRepoints(): void {
const db = this._orchestrationDb
// Before the scan, so a released batch is found as undelivered like any other.
releaseRestoredStructuredPointerClaims(db)
let handles: Set<string>
try {
const db = this._orchestrationDb
// Pointer-phase rows are excluded from the undelivered scan, so they need their own.
handles = new Set([
...(db?.getUndeliveredUnreadMailboxHandles?.() ?? []),
Expand Down
1 change: 1 addition & 0 deletions src/main/runtime/orca-runtime-get-worktree-ps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStructuredAgent
// Structured chat has no agent CLI hooks, so this projection is what the first-work
// workspace rename listens to instead of `agentStatus:set`.
onSessionStatusChanged: (summary, options) => {
this.onStructuredSessionStatusForMail(summary)
void maybeAutoRenameWorkspaceOnFirstStructuredTurn(
summary,
options,
Expand Down
7 changes: 5 additions & 2 deletions src/main/runtime/orca-runtime-stop-requested-pty-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { OrchestrationStructuredMailboxPointerDelivery } from './orchestration/structured-mailbox-pointer-delivery'
import { createStructuredMailboxPointerHost } from './orchestration/structured-mailbox-pointer-host'
import { isStructuredWorkerHandle } from './structured-worker-identity'
import { agentSessionPtyWriteGate } from './agent-session-pty-write-gate'
import { resolveStructuredWorkerAuthority } from './structured-worker-authority'
import { OrcaRuntimeWithRuntimeId } from './orca-runtime-runtime-id'
import { RuntimeTerminalAgentPresence } from './runtime-terminal-agent-presence'
Expand Down Expand Up @@ -183,7 +184,8 @@ export class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWithRuntimeId
onRoutedMessageTypes: (mailboxHandle, types) =>
this.messageWaiters.notifyRouted(mailboxHandle, types),
onForeignMailboxRouted: (mailboxHandle, messageType) =>
this.notifyMessageArrived(mailboxHandle, messageType)
this.notifyMessageArrived(mailboxHandle, messageType),
getBoundSessionIdForPty: (ptyId) => agentSessionPtyWriteGate.boundSessionId(ptyId)
})

protected readonly orchestrationMailboxDeliveryTarget = new OrchestrationMailboxDeliveryTarget({
Expand All @@ -193,7 +195,8 @@ export class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWithRuntimeId
isStructuredWorkerHandle: (handle) => isStructuredWorkerHandle(handle),
canProbePtyLiveness: () => Boolean(this.ptyController?.probePtyLiveness),
controllerKnowsPtyIsLive: (ptyId) => this.controllerKnowsPtyIsLive(ptyId),
isLeafPtyProvenAbsent: (ptyId) => this.isLeafPtyProvenAbsent(ptyId)
isLeafPtyProvenAbsent: (ptyId) => this.isLeafPtyProvenAbsent(ptyId),
getTerminalViewHandleForSession: (sessionId) => this.getTerminalViewHandleForSession(sessionId)
})

protected readonly orchestrationMailboxPointerDelivery = new OrchestrationMailboxPointerDelivery({
Expand Down
22 changes: 22 additions & 0 deletions src/main/runtime/orchestration/cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id'

export type OrchestrationCliCommand = 'orca' | 'orca-dev' | 'orca-ide'

/** The CLI invocation text a structured session's shell can run, one form per shell family. */
export type StructuredSessionCliInvocation = '"$ORCA_CLI_COMMAND"' | '& $env:ORCA_CLI_COMMAND'

/**
* How text addressed to a structured session invokes this app's CLI, in the shell that session's
* commands actually run in. The env carries the absolute launcher in `ORCA_CLI_COMMAND`; a bare
* `orca` can resolve elsewhere once a profile-loading shell rebuilds PATH ahead of Orca's entry.
*
* - Codex on Windows runs PowerShell (pwsh, else Windows PowerShell) and loads its profile, so the
* env var is read as `$env:…` and invoked with `&`. Codex falls back to cmd only when no
* PowerShell exists at all, which Orca cannot see from here.
* - Claude on Windows runs its commands in Git Bash; macOS and Linux shells are POSIX for both.
*/
export function structuredSessionCliInvocation(session: {
platform: NodeJS.Platform
provider: 'claude' | 'codex'
}): StructuredSessionCliInvocation {
return session.platform === 'win32' && session.provider === 'codex'
? '& $env:ORCA_CLI_COMMAND'
: '"$ORCA_CLI_COMMAND"'

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.

The Codex arm is now correct, but the win32 + claude fall-through still hardcodes the POSIX form on the premise that Claude on Windows always runs commands in Git Bash. Upstream Claude Code docs say Git Bash is optional: the PowerShell tool is primary when enabled, and PowerShell is the only shell when Git for Windows is absent — there "$ORCA_CLI_COMMAND" is a PowerShell parse error and the pointer turn cannot run check.

Orca already makes this exact distinction for Claude hooks: getWindowsManagedLifecycleHook (src/main/claude/hook-settings.ts:180-192) uses the direct POSIX command only when isGitBashAvailable(), otherwise it emits a PowerShell command. The pointer lane should branch the same way, or the JSDoc at cli-command.ts:18 should state the Git Bash precondition so the assumption is auditable.

Technical details
# Windows Claude structured sessions may receive an unrunnable pointer

## Affected sites
- `src/main/runtime/orchestration/cli-command.ts:24-26` — `win32` + `claude` returns `"$ORCA_CLI_COMMAND"`, treating Git Bash as guaranteed.
- `src/main/runtime/orchestration/cli-command.ts:18` — the JSDoc states that premise as fact.
- `src/main/runtime/orchestration/structured-mailbox-pointer-host.ts:85-91` — `cliInvocation` has the session record (so `process.platform` and `provider`) but no Git Bash availability.
- Precedent: `src/main/claude/hook-settings.ts:180-192` — `getWindowsManagedLifecycleHook` branches on `isGitBashAvailable()`.
- Availability source: `src/main/git-bash.ts:108` (`isGitBashAvailable`), already surfaced over RPC by `host-capabilities.ts`.

## Required outcome
- On a Windows host, a Claude structured session without Git Bash must receive a pointer its shell can run (`& $env:ORCA_CLI_COMMAND …`), or the POSIX mapping must be provably safe under Orca's structured-Claude launch.

## Suggested approach
- Thread Git Bash availability into `structuredSessionCliInvocation` (e.g. an optional `gitBashAvailable` on the session arg) and pass `isGitBashAvailable()` from `cliInvocation`; keep the POSIX form only when Git Bash is present.
- If Orca guarantees Git Bash for structured Claude on Windows, record that guarantee in the JSDoc.

## Open questions for the human
- Does Orca require Git for Windows for structured Claude on Windows, or can it run with the PowerShell tool as the primary shell?

}

export function resolveTerminalOrchestrationCliCommand(args: {
connectionId: string | null
isWsl: boolean | null | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ export function hasOutstandingMailboxDelivery(
)
}

/** The batch a consumer has read and not yet acknowledged on this mailbox, if any. */
export function getOutstandingMailboxDelivery(
this: OrchestrationDb,
mailboxHandle: string
): { id: string; messageIds: ReadonlySet<string> } | undefined {
const row: unknown = this.db
.prepare('SELECT id, message_ids FROM outstanding_deliveries WHERE mailbox_handle = ? LIMIT 1')
.get(mailboxHandle)
if (
!row ||
typeof row !== 'object' ||
!('id' in row) ||
typeof row.id !== 'string' ||
!('message_ids' in row) ||
typeof row.message_ids !== 'string'
) {
return undefined
}
const ids: unknown = JSON.parse(row.message_ids)
return {
id: row.id,
messageIds: new Set(Array.isArray(ids) ? ids.filter((id) => typeof id === 'string') : [])
}
}

export function fenceUnacknowledgedMailboxDeliveries(
this: OrchestrationDb,
mailboxHandle: string
Expand All @@ -201,6 +226,7 @@ export type RoleMailboxDeliveryMethods = {
getOrCreateMailboxDelivery: typeof getOrCreateMailboxDelivery
acknowledgeMailboxDelivery: typeof acknowledgeMailboxDelivery
hasOutstandingMailboxDelivery: typeof hasOutstandingMailboxDelivery
getOutstandingMailboxDelivery: typeof getOutstandingMailboxDelivery
fenceUnacknowledgedMailboxDeliveries: typeof fenceUnacknowledgedMailboxDeliveries
}

Expand All @@ -211,6 +237,7 @@ export function attachRoleMailboxDelivery(ctor: { prototype: object }): void {
getOrCreateMailboxDelivery,
acknowledgeMailboxDelivery,
hasOutstandingMailboxDelivery,
getOutstandingMailboxDelivery,
fenceUnacknowledgedMailboxDeliveries
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,43 @@ export function deleteStructuredPointerOperation(
.run(mailboxHandle)
}

export function listStructuredPointerOperations(
this: OrchestrationDb
): StructuredPointerOperationRow[] {
const rows = this.db.prepare('SELECT * FROM structured_pointer_operations').all()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SELECT * over this table returns the row shape its schema and row type define, like every row cast in db/.
return rows as StructuredPointerOperationRow[]
}

/** Unread rows a pointer was sent for, in the order a pointer batch lists them. */
export function getPointedUnreadMessages(
this: OrchestrationDb,
mailboxHandle: string
): { id: string; delivered_at: string }[] {
const rows = this.db
.prepare(
`SELECT id, delivered_at FROM messages
WHERE to_handle = ? AND read = 0 AND delivered_at IS NOT NULL
AND delivery_contract = 'current_delivery'
ORDER BY sequence`
)
.all(mailboxHandle)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: both columns are TEXT and the WHERE clause excludes a NULL `delivered_at`.
return rows as { id: string; delivered_at: string }[]
}

export type StructuredPointerOperationStoreMethods = {
listStructuredPointerOperations: typeof listStructuredPointerOperations
getPointedUnreadMessages: typeof getPointedUnreadMessages
getStructuredPointerOperation: typeof getStructuredPointerOperation
putStructuredPointerOperation: typeof putStructuredPointerOperation
deleteStructuredPointerOperation: typeof deleteStructuredPointerOperation
}

export function attachStructuredPointerOperationStore(ctor: { prototype: object }): void {
Object.assign(ctor.prototype, {
listStructuredPointerOperations,
getPointedUnreadMessages,
getStructuredPointerOperation,
putStructuredPointerOperation,
deleteStructuredPointerOperation
Expand Down
9 changes: 6 additions & 3 deletions src/main/runtime/orchestration/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MessageRow } from './types'
import { ORCHESTRATION_LEGACY_RUN_ID } from '../../../shared/orchestration-rpc-contract'
import type { OrchestrationCliCommand } from './cli-command'
import type { OrchestrationCliCommand, StructuredSessionCliInvocation } from './cli-command'

const BANNER_WIDTH = 60
const SEPARATOR = '─'.repeat(BANNER_WIDTH)
Expand Down Expand Up @@ -112,11 +112,14 @@ export function formatMessagesForInjection(messages: MessageRow[]): string {
export function formatMessagePointer(
count: number,
mailboxHandle?: string,
cliCommand: OrchestrationCliCommand = 'orca'
cliCommand: OrchestrationCliCommand | StructuredSessionCliInvocation = 'orca',
/** The unacknowledged batch the reader holds; `check` replays it until acked. */
ackDeliveryId?: string
): string {
const noun = count === 1 ? 'message' : 'messages'
const runFlag = mailboxHandle?.startsWith('run:')
? ` --run ${mailboxHandle.slice('run:'.length)}`
: ''
return `\nYou have ${count} orchestration ${noun}. Run \`${cliCommand} orchestration check${runFlag}\`.\n`
const ackFlag = ackDeliveryId ? ` --ack ${ackDeliveryId}` : ''
return `\nYou have ${count} ${ackDeliveryId ? 'new ' : ''}orchestration ${noun}. Run \`${cliCommand} orchestration check${runFlag}${ackFlag}\`.\n`
}
9 changes: 8 additions & 1 deletion src/main/runtime/orchestration/mailbox-delivery-target.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { OrchestrationDb } from './db'
import type { OrchestrationMailboxLeaf } from './mailbox-owner'
import { handleLessCoordinatorSessionId } from './structured-session-mail-target'

type OrchestrationMailboxDeliveryTargetDependencies = {
getDb: () => OrchestrationDb | null
Expand All @@ -10,6 +11,8 @@ type OrchestrationMailboxDeliveryTargetDependencies = {
canProbePtyLiveness: () => boolean
controllerKnowsPtyIsLive: (ptyId: string) => boolean
isLeafPtyProvenAbsent: (ptyId: string) => Promise<boolean>
/** The terminal of a structured session's terminal view, while a TUI owns that session. */
getTerminalViewHandleForSession?: (sessionId: string) => string | null
}

export class OrchestrationMailboxDeliveryTarget {
Expand All @@ -31,8 +34,12 @@ export class OrchestrationMailboxDeliveryTarget {
const remote =
dispatchId && !dispatch ? db?.getRemoteDispatchAttachment?.(dispatchId) : undefined
const paneKey = dispatch?.assignee_pane_key ?? remote?.pane_key
const run = runId ? db?.getRun(runId) : undefined
const coordinatorSessionId = run ? handleLessCoordinatorSessionId(run) : null
const ownerHandle = runId
? db?.getRun(runId)?.coordinator_handle
? coordinatorSessionId
? this.deps.getTerminalViewHandleForSession?.(coordinatorSessionId)
: run?.coordinator_handle
: ((paneKey ? this.deps.getTerminalHandleForPaneKey(paneKey) : null) ??
dispatch?.assignee_handle ??
remote?.terminal_handle)
Expand Down
Loading
Loading