Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
77254c9
fix(agent-presence): project execution-host ownership across remote c…
brennanb2025 Sep 30, 2026
98fd831
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Sep 30, 2026
988a397
refactor(agent-presence): move hookless and Windows capture out of st…
brennanb2025 Sep 30, 2026
30aef02
Merge commit '5d386b5a2b3' into brennanb2025/agent-presence-step3
brennanb2025 Sep 30, 2026
49cc265
fix(agent-presence): keep permission and management checks ahead of h…
brennanb2025 Sep 30, 2026
6e81d25
fix(agent-presence): derive paired-client presence per host snapshot
brennanb2025 Sep 30, 2026
4d55d58
fix(agent-presence): let a host frame for a new owner process replace…
brennanb2025 Sep 30, 2026
f03b3f4
refactor(agent-presence): drop the unread ps presence field and the d…
brennanb2025 Sep 30, 2026
c08f8d8
fix(agent-presence): treat an exited owner as history on the phone an…
brennanb2025 Sep 30, 2026
20c3343
test(agent-presence): use a real explicit status state in the headles…
brennanb2025 Sep 30, 2026
1bea899
fix(agent-presence): let a shell in front veto a live owner for keybo…
brennanb2025 Sep 30, 2026
928a956
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Sep 30, 2026
78d9746
refactor(agent-presence): decide relay-owned frames in one place
brennanb2025 Sep 30, 2026
7fd2b5c
fix(agent-presence): keep host provenance off stored status rows
brennanb2025 Sep 30, 2026
05f873b
fix(agent-presence): add host presence to the published tab status in…
brennanb2025 Sep 30, 2026
abfdb22
fix(agent-presence): keep host-local rows local on the paired client
brennanb2025 Sep 30, 2026
4773e17
fix(agent-presence): project an exited owner as identity only
brennanb2025 Sep 30, 2026
a8b41aa
fix(agent-presence): release a mirrored owner when the host publishes…
brennanb2025 Sep 30, 2026
01dd4ed
perf(agent-presence): republish mirrored presence only when the owner…
brennanb2025 Sep 30, 2026
41071c4
fix(agent-presence): keep main's keyboard rules for owners recorded o…
brennanb2025 Sep 30, 2026
d3d189a
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Sep 30, 2026
4147516
fix(agent-presence): let the owner answer for the keyboard only when …
brennanb2025 Sep 30, 2026
cee22f2
fix(agent-presence): accept a replayed host exit only against a diffe…
brennanb2025 Sep 30, 2026
84a1bcc
test(agent-presence): pin the headless owner as identity only, not as…
brennanb2025 Sep 30, 2026
79ee51c
fix(agent-presence): order mirrored owners by snapshot and keep a lat…
brennanb2025 Sep 30, 2026
2e9c59f
fix(agent-presence): advertise the owner record only from clients tha…
brennanb2025 Sep 30, 2026
64bc396
fix(agent-presence): keep published agent statuses stable when stripp…
brennanb2025 Sep 30, 2026
5c44f10
refactor(protocol): move browser capability constants to their own mo…
brennanb2025 Oct 1, 2026
a6ec791
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Oct 1, 2026
e6353da
fix(ssh): forward the relay's hook envelope to main as the WSL link does
brennanb2025 Oct 1, 2026
4cb151c
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Oct 1, 2026
351af5a
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Oct 1, 2026
fc4d138
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Oct 2, 2026
be3ad8a
Merge remote-tracking branch 'origin/brennanb2025/agent-presence-step…
brennanb2025 Oct 2, 2026
3f2a5cb
test(agent-presence): give the mobile presence test host the title di…
brennanb2025 Oct 3, 2026
1e7ef81
refactor(runtime): move pane status-row lookups out of runtime-mobile…
brennanb2025 Oct 3, 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
47 changes: 47 additions & 0 deletions mobile/src/session/mobile-agent-presence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
import { resolveMobileNativeChat } from './mobile-native-chat-eligibility'
import { resolveMobileTerminalTabAgentId } from './mobile-terminal-tab-agent'

function tab(ended?: true) {
const agentStatus: AgentStatusEntry = {
state: 'done',
prompt: '',
paneKey: 'tab:leaf',
updatedAt: 1,
stateStartedAt: 1,
stateHistory: [],
agentType: 'claude',
providerSession: { key: 'session_id', id: 'session' },
agentPresence: {
agent: 'claude',
process: { pid: 42, platform: 'linux', startTime: 'boot:42' },
...(ended ? { ended } : {})
}
}
return { type: 'terminal', title: 'claude', launchAgent: 'claude' as const, agentStatus }
}
describe('host process presence on mobile', () => {
it('retains an idle owner under a shell title', () => {
const live = { ...tab(), title: 'zsh' }
expect(resolveMobileTerminalTabAgentId(live)).toBe('claude')
expect(resolveMobileNativeChat(live)?.sessionId).toBe('session')
})
it('does not revive an ended owner from title, launch or transcript identity', () => {
expect(resolveMobileTerminalTabAgentId(tab(true))).toBeNull()
expect(resolveMobileNativeChat(tab(true))).toBeNull()
})
it('shows a hookless agent started after the owner exited, as it would without presence', () => {
const successor = { ...tab(true), title: 'aider' }
expect(resolveMobileTerminalTabAgentId(successor)).toBe('aider')
const withoutPresence = { ...successor, agentStatus: undefined, launchAgent: undefined }
expect(resolveMobileTerminalTabAgentId(withoutPresence)).toBe('aider')
})
it('keeps old-host and unidentified rows on the legacy path', () => {
const legacy = tab()
delete legacy.agentStatus.agentPresence
expect(resolveMobileNativeChat(legacy)?.agent).toBe('claude')
legacy.agentStatus.agentPresence = { agent: 'claude', ended: true }
expect(resolveMobileTerminalTabAgentId(legacy)).toBe('claude')
})
})
10 changes: 8 additions & 2 deletions mobile/src/session/mobile-native-chat-eligibility.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
paneEvidenceAgent,
selectLiveOwnerAgent
} from '../../../src/shared/ended-agent-owner-evidence'
import { isAgentSessionHandleProvider } from '../../../src/shared/agent-session-provider-handle'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
import { isRuntimeOwnedSshTargetId } from '../../../src/shared/execution-host'
Expand Down Expand Up @@ -58,12 +62,14 @@ export function resolveMobileNativeChat(
if (tab.type !== 'terminal') {
return null
}
const liveAgent = tab.agentStatus?.agentType ?? null
const presence = tab.agentStatus?.agentPresence
const liveAgent =
selectLiveOwnerAgent(presence) ?? paneEvidenceAgent(presence, tab.agentStatus?.agentType)
const agent = liveAgent
? isNativeChatSupportedAgent(liveAgent)
? liveAgent
: null
: tab.launchAgent
: paneEvidenceAgent(presence, tab.launchAgent)
if (!agent || !isNativeChatSupportedAgent(agent)) {
return null
}
Expand Down
27 changes: 20 additions & 7 deletions mobile/src/session/mobile-terminal-tab-agent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
paneEvidenceAgent,
selectLiveOwnerAgent
} from '../../../src/shared/ended-agent-owner-evidence'
import { stripLeadingAgentTitleDecorationOrEmpty } from '../../../src/shared/agent-title-decoration'
import { resolveExplicitTerminalTitleAgentType } from '../../../src/shared/terminal-title-agent-type'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
Expand All @@ -17,22 +21,28 @@ import type { MobileSessionTab } from './mobile-session-route-types'
*/
type MobileTerminalTabAgentIdentity = {
title: string
agentStatus?: { agentType?: AgentStatusEntry['agentType'] | null } | null
agentStatus?: {
agentType?: AgentStatusEntry['agentType'] | null
agentPresence?: AgentStatusEntry['agentPresence']
} | null
launchAgent?: TuiAgent | null
}

/** Agent identity Orca owns, excluding the display-only title fallback. */
export function resolveMobileTerminalTabOwnedAgentId(
tab: MobileTerminalTabAgentIdentity
): string | null {
const hookAgentType = tab.agentStatus?.agentType?.trim()
const presence = tab.agentStatus?.agentPresence
const owner = selectLiveOwnerAgent(presence)
if (owner) {
return owner
}
// Why: an exited owner's own hook row and launch intent are history; other agents still show.
const hookAgentType = paneEvidenceAgent(presence, tab.agentStatus?.agentType?.trim())
if (hookAgentType && hookAgentType !== 'unknown') {
return hookAgentType
}
if (tab.launchAgent) {
return tab.launchAgent
}
return null
return paneEvidenceAgent(presence, tab.launchAgent)
}

export function resolveMobileTerminalTabAgentId(
Expand All @@ -42,7 +52,10 @@ export function resolveMobileTerminalTabAgentId(
if (ownedAgent) {
return ownedAgent
}
return resolveExplicitTerminalTitleAgentType(tab.title)
return paneEvidenceAgent(
tab.agentStatus?.agentPresence,
resolveExplicitTerminalTitleAgentType(tab.title)
)
}

export function getMobileSessionTabTitle(tab: MobileSessionTab): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
AGENT_LAUNCH_RUNTIME_CAPABILITY,
AGENT_PROCESS_PRESENCE_RUNTIME_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
SESSION_TABS_SPLIT_GROUP_PLACEMENT_RUNTIME_CAPABILITY,
Expand Down Expand Up @@ -34,6 +35,10 @@ describe('mobile runtime client capabilities', () => {
expect(MOBILE_RUNTIME_CLIENT_CAPABILITIES).toContain(AGENT_LAUNCH_RUNTIME_CAPABILITY)
})

it('opts into the host owner record that tab icons and Chat read', () => {
expect(MOBILE_RUNTIME_CLIENT_CAPABILITIES).toContain(AGENT_PROCESS_PRESENCE_RUNTIME_CAPABILITY)
})

it('stays inside the bounds the host parses, which fail closed to no capabilities at all', () => {
expect(MOBILE_RUNTIME_CLIENT_CAPABILITIES.length).toBeLessThanOrEqual(HOST_CAPABILITY_LIMIT)
for (const capability of MOBILE_RUNTIME_CLIENT_CAPABILITIES) {
Expand Down
5 changes: 4 additions & 1 deletion mobile/src/transport/mobile-runtime-client-capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AGENT_LAUNCH_RUNTIME_CAPABILITY,
AGENT_PROCESS_PRESENCE_RUNTIME_CAPABILITY,
AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
Expand All @@ -20,7 +21,9 @@ export const MOBILE_RUNTIME_CLIENT_CAPABILITIES = remoteRuntimeClientCapabilitie
// Mobile renders either launch outcome — a structured chat or a terminal agent — so it may ask
// the host to pick. Without this the host refuses `agent.launch` and every mobile create with an
// agent stays a PTY.
AGENT_LAUNCH_RUNTIME_CAPABILITY
AGENT_LAUNCH_RUNTIME_CAPABILITY,
// Tab icons and Chat read the host's owner record; without it the host sends the legacy row.
AGENT_PROCESS_PRESENCE_RUNTIME_CAPABILITY
])

export const MOBILE_RUNTIME_CLIENT_CAPABILITY_UPDATE_METHOD =
Expand Down
2 changes: 1 addition & 1 deletion src/cli/handlers/browser-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
BrowserIdentityModeStatus,
BrowserUserAgentMode
} from '../../shared/browser-user-agent-mode'
import { BROWSER_IDENTITY_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
import { BROWSER_IDENTITY_RUNTIME_CAPABILITY } from '../../shared/browser-runtime-capabilities'
import type { RuntimeStatus } from '../../shared/runtime-types'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { getRequiredStringFlag } from '../flags'
Expand Down
Loading
Loading