mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +00:00
refactor(terminal): name the provenance rule the takeover fence hangs off
The fence rode the mobile input floor claim, with only a comment tying the two together. The floor is arbitration -- who may write next -- while the fence needs provenance -- who produced the bytes. They agree today, so anyone reweighing the floor would have moved the fence without noticing. isDeliberateHumanInput states the provenance rule on its own terms, and both byte lanes decide with it when they open a write: the claim carries the verdict beside the handle, and settlement records the takeover only when a human produced the bytes. No behavior change -- afterWrite is wired only where the predicate already answers true -- and the rule is now pinned by its own cases, so a future arbitration change has to answer this question again rather than inherit it.
This commit is contained in:
+20
-1
@@ -1,7 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createOrchestrationWorkerReleaseHarness } from './worker-release.test-support'
|
||||
import { TERMINAL_SEND_METHODS } from '../../terminal/terminal-send-method'
|
||||
import { sendTerminalStreamInput } from '../../terminal/terminal-input-delivery'
|
||||
import {
|
||||
isDeliberateHumanInput,
|
||||
sendTerminalStreamInput
|
||||
} from '../../terminal/terminal-input-delivery'
|
||||
import { isStreamingMethod, type RpcMethod } from '../../../core'
|
||||
import { RuntimeTerminalWriter } from '../../../../runtime-terminal-writer'
|
||||
import { getDefaultWorkspaceSession } from '../../../../../../shared/constants'
|
||||
@@ -324,3 +327,19 @@ describe('a mobile takeover lifts the settled worker resume fence', () => {
|
||||
expect(fenceChanges.at(-1)).toEqual([harness.workerPaneKey, false])
|
||||
})
|
||||
})
|
||||
|
||||
// The fence hangs off provenance, not off the input floor, so this rule is pinned on its own.
|
||||
describe('which bytes count as a person typing', () => {
|
||||
const cases: [string, Parameters<typeof isDeliberateHumanInput>, boolean][] = [
|
||||
['a phone keystroke', [{ client: MOBILE_CLIENT }, false], true],
|
||||
["an agent's terminal send", [{ client: CLI_CLIENT }, false], false],
|
||||
['a phone query reply', [{ client: MOBILE_CLIENT, inputKind: 'query-reply' }, false], false],
|
||||
['a legacy phone on a mobile-driven pane', [{}, true], true],
|
||||
['a clientless send on an idle pane', [{}, false], false]
|
||||
]
|
||||
for (const [name, args, expected] of cases) {
|
||||
it(`${name} is ${expected ? '' : 'not '}human input`, () => {
|
||||
expect(isDeliberateHumanInput(...args)).toBe(expected)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -51,6 +51,42 @@ export function resolveMobileFloorClientId(
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether these bytes are a person typing, rather than an agent's `terminal send` or the emulator
|
||||
* answering a device query.
|
||||
*
|
||||
* Deliberately its own rule instead of a read of the mobile input floor. The floor is arbitration,
|
||||
* deciding who may write next; this is provenance, deciding who produced the bytes. They agree
|
||||
* today, and the orchestration takeover fence hangs off THIS one, so reweighing the floor cannot
|
||||
* move the fence without someone answering this question again on its own terms.
|
||||
*/
|
||||
export function isDeliberateHumanInput(
|
||||
input: { client?: TerminalViewportClient; inputKind?: 'query-reply' },
|
||||
mobileWithoutClientMetadata: boolean
|
||||
): boolean {
|
||||
if (input.inputKind === 'query-reply') {
|
||||
return false
|
||||
}
|
||||
if (input.client?.type === 'mobile') {
|
||||
return true
|
||||
}
|
||||
// Pre-refactor mobile builds send no client metadata; the pane's mobile driver, or a mobile
|
||||
// stream's own kind, is the only remaining evidence of who is at the keyboard.
|
||||
return !input.client && mobileWithoutClientMetadata
|
||||
}
|
||||
|
||||
/** One mobile write's floor claim and provenance verdict, decided together before the bytes move. */
|
||||
export function newMobileInputWrite(
|
||||
input: { terminal: string; client?: TerminalViewportClient; inputKind?: 'query-reply' },
|
||||
mobileWithoutClientMetadata: boolean
|
||||
): MobileInputFloorClaimHolder {
|
||||
return {
|
||||
handle: input.terminal,
|
||||
humanInput: isDeliberateHumanInput(input, mobileWithoutClientMetadata),
|
||||
current: null
|
||||
}
|
||||
}
|
||||
|
||||
export type TerminalStreamInputOutcome = 'delivered' | 'rejected' | 'failed'
|
||||
|
||||
export function watchSubscriptionLifetime(
|
||||
@@ -113,7 +149,7 @@ export async function sendTerminalStreamInput(
|
||||
): Promise<TerminalStreamInputOutcome> {
|
||||
const action = { text: args.text, enter: false, interrupt: false }
|
||||
const clientId = args.isMobile ? args.client?.id : undefined
|
||||
const floorClaim: MobileInputFloorClaimHolder = { handle: args.terminal, current: null }
|
||||
const floorClaim = newMobileInputWrite(args, args.isMobile)
|
||||
try {
|
||||
if (!clientId) {
|
||||
const result = await runtime.sendTerminal(args.terminal, action)
|
||||
@@ -142,22 +178,21 @@ export async function sendTerminalStreamInput(
|
||||
|
||||
export type MobileInputFloorClaimHolder = {
|
||||
handle: string
|
||||
humanInput: boolean
|
||||
current: ReturnType<OrcaRuntimeService['beginMobileInputFloor']>
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle an accepted mobile write: the phone keeps the input floor, and the host records that a
|
||||
* human is now driving this terminal.
|
||||
*
|
||||
* The floor claim is the host's only proof the bytes are deliberate human input — an agent's
|
||||
* `terminal send` reaches the same methods naming itself a desktop client, and the emulator's own
|
||||
* query replies never claim the floor — so the takeover fence hangs off exactly this condition.
|
||||
* Settle an accepted mobile write: the phone keeps the input floor, and if a human produced the
|
||||
* bytes the host records that they are now driving this terminal.
|
||||
*/
|
||||
export async function settleMobileInputWrite(
|
||||
runtime: OrcaRuntimeService,
|
||||
claim: MobileInputFloorClaimHolder
|
||||
): Promise<void> {
|
||||
recordWorkerTerminalUserTakeoverFromInput(runtime, claim.handle)
|
||||
if (claim.humanInput) {
|
||||
recordWorkerTerminalUserTakeoverFromInput(runtime, claim.handle)
|
||||
}
|
||||
await commitMobileInputFloorClaim(claim)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
getTerminalSendGuardRefusedReason,
|
||||
isTerminalInputLockedForClient,
|
||||
isTerminalSendGuardNotWritable,
|
||||
newMobileInputWrite,
|
||||
resolveMobileFloorClientId,
|
||||
settleMobileInputWrite,
|
||||
type MobileInputFloorClaimHolder
|
||||
settleMobileInputWrite
|
||||
} from './terminal-input-delivery'
|
||||
import { updateViewportForClient } from './terminal-viewport-update'
|
||||
import {
|
||||
@@ -181,7 +181,7 @@ export const TERMINAL_SEND_METHODS: RpcAnyMethod[] = [
|
||||
}
|
||||
}
|
||||
const mobileFloorClientId = resolveMobileFloorClientId(driver, params.client)
|
||||
const floorClaim: MobileInputFloorClaimHolder = { handle: params.terminal, current: null }
|
||||
const floorClaim = newMobileInputWrite(params, driver?.kind === 'mobile')
|
||||
const beforeWrite =
|
||||
orchestrationMutation && params.agentPrompt === true
|
||||
? async (ptyId?: string): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user