perf(runtime): gate PTY path-candidate extraction on mobile-connected (#9422)

* perf(runtime): gate PTY path-candidate extraction on mobile-connected

The 3-regex path extractor ran on every PTY chunk of every session for a mobile-only consumer. Now gated behind a sticky mobile-connected flag; first onReady synchronously backfills candidates from the retained 64KB window per original chunk boundary (lazy safety-net fallback). ~28.5us/chunk (~99.8%) saved on the desktop-only hot path; mobile parity preserved (Tier-1 raw-window read unchanged + backfill before any file RPC). 5-round review converged 2-consecutive-clean; independently verified tsc+902 tests.

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): guard path-candidate activation on the remote runtime proxy

MobileSocketWiring.onReady called this.runtime.activateRecentPtyPathCandidateTracking() unconditionally, throwing on the E2EE/remote proxy (which lacks the method) and breaking the remote runtime connection (CI verify failure). Guarded with optional chaining (runtime-rpc.ts:900) — activation is a local-host concern. Mutation-verified E2EE-proxy regression test. Integration test + 3 typechecks green.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-20 23:59:09 -07:00
committed by GitHub
co-authored by Orca
parent 4a7bdf9177
commit 58bcfe2a33
6 changed files with 535 additions and 12 deletions
+175
View File
@@ -8580,6 +8580,181 @@ describe('OrcaRuntimeService', () => {
)
})
it('resolves paths without candidate activation via the lazy safety net', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/lazy-activation-artifact.json'
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100)
// No mobile connect ever happened; the query itself must activate+backfill.
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('backfills candidates on activation so scrolled-off paths still resolve', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/backfilled-artifact.json'
// Path arrives while tracking is inactive (desktop-only phase).
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100)
// First mobile connect: backfill from the retained raw window.
runtime.activateRecentPtyPathCandidateTracking()
// Scroll the raw 64KB window past the path with pathless output.
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
// Only the backfilled candidate tier can answer now.
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('backfills per retained chunk so chunk boundaries match the eager extractor', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/a.json'
// Two chunks whose join would parse as one different candidate
// (/tmp/a.jsonsuffix.txt). The eager per-chunk extractor kept /tmp/a.json.
runtime.onPtyData('pty-1', `wrote ${artifactPath}`, 100)
runtime.onPtyData('pty-1', 'suffix.txt', 150)
runtime.activateRecentPtyPathCandidateTracking()
// Scroll the raw 64KB window so only the backfilled candidates can answer.
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('extracts candidates per chunk after activation for scrolled-off paths', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/post-activation-artifact.json'
runtime.activateRecentPtyPathCandidateTracking()
// Idempotent: a second activation must not disturb live tracking.
runtime.activateRecentPtyPathCandidateTracking()
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100)
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('does not retain pre-activation paths that scrolled past the raw window', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/pre-activation-scrolled-artifact.json'
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100)
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
// Documented accepted loss: output that scrolled past the raw window
// before the first-ever mobile connect yields no candidates.
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
false
)
})
it('backfill does not mint candidates from an over-limit line shortened by the window trim', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/result.json'
// One chunk with a >4KiB line whose tail is the path: the eager
// extractor skipped it under the line-length guard.
runtime.onPtyData('pty-1', `${'a'.repeat(5000)} ${artifactPath}\n`, 100)
// Newline-free filler trims the window to ~1KiB before the path, so a
// trimmed-head replay would see an under-limit line ending in the path.
runtime.onPtyData('pty-1', 'y'.repeat(64 * 1024 - 1000), 150)
runtime.activateRecentPtyPathCandidateTracking()
// Scroll the raw window so only backfilled candidates can answer.
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
// Parity with eager extraction: the over-limit line never yielded a
// candidate, so the grant must stay denied after the raw window scrolls.
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
false
)
})
it('backfill replays the full head chunk including its window-trimmed prefix', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/trimmed-prefix-artifact.json'
// Path sits in the head chunk's prefix, which the window trim drops from
// read() but the eager extractor saw at append time.
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n${'b'.repeat(3000)}\n`, 100)
runtime.onPtyData('pty-1', 'y'.repeat(64 * 1024 - 1000), 150)
runtime.activateRecentPtyPathCandidateTracking()
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
// Parity with eager extraction: the append-time candidate outlived the
// raw window, so backfill must recover it from the intact head chunk.
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('matches eager extraction exactly for a pre-sliced oversized chunk', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const cutLinePath = '/tmp/cut-line.json'
const keptPath = '/tmp/kept-after-cut.json'
// Single >64KiB append is stored pre-sliced, so its original text is
// unrecoverable at activation time. Extraction runs eagerly at append
// instead: cutLinePath sat on an over-4KiB line the extractor's line
// guard rejects (and the slice leaves an under-4KiB tail of it that must
// NOT mint a candidate later), while keptPath sat on a short line and
// must survive the raw window scrolling.
const keptLine = `wrote ${keptPath}\n`
const afterFirstLine = `${keptLine}${'z'.repeat(62 * 1024 - keptLine.length)}`
const oversized = `${'a'.repeat(5 * 1024)} ${cutLinePath}\n${afterFirstLine}`
runtime.onPtyData('pty-1', oversized, 100)
runtime.activateRecentPtyPathCandidateTracking()
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, cutLinePath, cutLinePath)).toBe(
false
)
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, keptPath, keptPath)).toBe(true)
})
it('keeps a candidate from the short first line of an oversized chunk after the window scrolls', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
const artifactPath = '/tmp/result.json'
// A short first line of a >64KiB chunk loses only its `wrote ` prefix to
// the pre-slice; the path itself stays in the retained window. The old
// eager extractor recorded it from the intact original chunk, so it must
// stay authorized after the raw window scrolls — parity requires the
// append-time extraction for oversized chunks, not backfill replay.
const firstLine = `wrote ${artifactPath}\n`
runtime.onPtyData('pty-1', `${firstLine}${'f'.repeat(64 * 1024 + 6 - firstLine.length)}`, 100)
runtime.activateRecentPtyPathCandidateTracking()
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe(
true
)
})
it('replaces suffix-only headless state with the recovered renderer snapshot', async () => {
const runtime = createRuntime()
syncSinglePty(runtime, 'pty-1')
+61 -6
View File
@@ -370,7 +370,7 @@ import type {
import type { AutomationService } from '../automations/service'
import { RuntimeBrowserCommands } from './orca-runtime-browser'
import { buildHeadlessTerminalSplitLayout } from './headless-terminal-split-layout'
import { RecentPtyOutputBuffer } from './recent-pty-output-buffer'
import { RECENT_PTY_OUTPUT_LIMIT, RecentPtyOutputBuffer } from './recent-pty-output-buffer'
import {
buildHeadlessTabGroupMove,
buildHeadlessTabGroupSplit
@@ -2417,6 +2417,10 @@ export class OrcaRuntimeService {
private ptyLifecycleGenerationById = new Map<string, number>()
private nextPtyLifecycleGeneration = 1
private recentPtyPathCandidatesById = new Map<string, string[]>()
// Why: candidates only feed mobile file-tap provenance; desktop-only
// sessions skip the 3-regex extraction on every PTY chunk until a
// mobile/remote client authenticates (sticky, backfilled on activation).
private recentPtyPathCandidateTrackingActive = false
// Why: OSC 9999 status can span PTY chunks. Keeping parser state in the
// runtime lets hidden/model-owned terminals observe agent state without a
// mounted xterm view.
@@ -8707,14 +8711,60 @@ export class OrcaRuntimeService {
private recordRecentPtyOutputForPathProvenance(ptyId: string, data: string): void {
let recentOutputBuffer = this.recentPtyOutputById.get(ptyId)
if (!recentOutputBuffer) {
recentOutputBuffer = new RecentPtyOutputBuffer()
// Boundaries are only owed to the one-time activation backfill; once
// tracking is live, new buffers keep the read-collapsing hot path.
recentOutputBuffer = new RecentPtyOutputBuffer({
preserveChunkBoundaries: !this.recentPtyPathCandidateTrackingActive
})
this.recentPtyOutputById.set(ptyId, recentOutputBuffer)
}
recentOutputBuffer.append(data)
this.recentPtyPathCandidatesById.set(
ptyId,
appendRecentPtyPathCandidates(this.recentPtyPathCandidatesById.get(ptyId), data)
)
if (
this.recentPtyPathCandidateTrackingActive ||
// Why: an over-window chunk is stored pre-sliced, so activation backfill
// could never replay its original text. Extract while intact; oversized
// chunks are rare, so the desktop-only gate still skips the hot path.
data.length > RECENT_PTY_OUTPUT_LIMIT
) {
this.recentPtyPathCandidatesById.set(
ptyId,
appendRecentPtyPathCandidates(this.recentPtyPathCandidatesById.get(ptyId), data)
)
}
}
activateRecentPtyPathCandidateTracking(): void {
if (this.recentPtyPathCandidateTrackingActive) {
return
}
this.recentPtyPathCandidateTrackingActive = true
// Why: synchronous backfill from the retained raw windows so a file tap
// right after first mobile connect resolves exactly as before the gate.
// Replay each retained chunk in its original full form: joining or
// trimming chunks would change the candidate set (e.g. a window cut can
// shorten an over-4KiB line under the extractor's line guard, minting
// candidates the eager extractor rejected).
// Accepted best-effort loss: output that scrolled past the raw window
// before the first-ever connect no longer yields candidates.
for (const [ptyId, buffer] of this.recentPtyOutputById) {
let candidates = this.recentPtyPathCandidatesById.get(ptyId)
const { chunks, headChunkIsPartial } = buffer.retainedChunks()
for (let index = 0; index < chunks.length; index += 1) {
if (index === 0 && headChunkIsPartial) {
// A pre-sliced over-window chunk was already extracted eagerly at
// append time (while its original text was intact); replaying its
// truncated remainder would mint or drop candidates spuriously.
continue
}
candidates = appendRecentPtyPathCandidates(candidates, chunks[index]!)
}
if (candidates) {
this.recentPtyPathCandidatesById.set(ptyId, candidates)
}
// Chunk boundaries were owed only to this one-time backfill; return
// the buffer to the compact read-collapsing steady state.
buffer.compact()
}
}
resolveTerminalContext(
@@ -8757,6 +8807,11 @@ export class OrcaRuntimeService {
}
hasRecentTerminalOutputPath(handle: string, pathText: string, absolutePath: string): boolean {
// Why: safety net for any query path that never saw a mobile onReady —
// lazily backfill so the answer matches pre-gate behavior.
if (!this.recentPtyPathCandidateTrackingActive) {
this.activateRecentPtyPathCandidateTracking()
}
const ptyId = this.resolveLeafForHandle(handle)?.ptyId
const recentOutput = ptyId ? this.recentPtyOutputById.get(ptyId)?.read() : null
if (recentOutput && recentTerminalOutputIncludesPath(recentOutput, pathText, absolutePath)) {
@@ -165,6 +165,83 @@ describe('RecentPtyOutputBuffer', () => {
expect(buffer.read()).toBe(reference ?? '')
})
it('retainedChunks preserves original chunk boundaries within the window', () => {
const buffer = new RecentPtyOutputBuffer()
buffer.append('wrote /tmp/a.json')
buffer.append('suffix.txt')
expect(buffer.retainedChunks().chunks).toEqual(['wrote /tmp/a.json', 'suffix.txt'])
expect(buffer.read()).toBe('wrote /tmp/a.jsonsuffix.txt')
// read() must not collapse the chunk array before compact(): boundaries
// survive a read so the backfill can still replay original chunks.
expect(buffer.retainedChunks().chunks).toEqual(['wrote /tmp/a.json', 'suffix.txt'])
})
it('retainedChunks keeps the full original head chunk when the window trims mid-chunk', () => {
const buffer = new RecentPtyOutputBuffer()
const head = 'a'.repeat(RECENT_PTY_OUTPUT_LIMIT - 5)
buffer.append(head)
buffer.append('bbbbbbbbbb')
buffer.append('cc')
const { chunks, headChunkIsPartial } = buffer.retainedChunks()
// The head chunk is intact (its original text, trimmed prefix included)
// so candidate backfill sees the same input the eager extractor saw.
expect(chunks).toEqual([head, 'bbbbbbbbbb', 'cc'])
expect(headChunkIsPartial).toBe(false)
expect(buffer.read()).toBe(`${'a'.repeat(RECENT_PTY_OUTPUT_LIMIT - 12)}bbbbbbbbbbcc`)
})
it('retainedChunks flags a pre-sliced oversized head chunk as partial', () => {
const buffer = new RecentPtyOutputBuffer()
buffer.append('z'.repeat(RECENT_PTY_OUTPUT_LIMIT + 100))
expect(buffer.retainedChunks().headChunkIsPartial).toBe(true)
buffer.append('tail')
expect(buffer.retainedChunks().headChunkIsPartial).toBe(true)
// Once the partial head chunk is fully evicted the flag clears.
buffer.append('w'.repeat(RECENT_PTY_OUTPUT_LIMIT - 1))
const { chunks, headChunkIsPartial } = buffer.retainedChunks()
expect(headChunkIsPartial).toBe(false)
expect(chunks).toEqual(['tail', 'w'.repeat(RECENT_PTY_OUTPUT_LIMIT - 1)])
// An exactly cap-sized append is stored whole, not partial.
const exact = new RecentPtyOutputBuffer()
exact.append('q'.repeat(RECENT_PTY_OUTPUT_LIMIT))
expect(exact.retainedChunks().headChunkIsPartial).toBe(false)
})
it('compact collapses to a single chunk and restores read-time defragmentation', () => {
const buffer = new RecentPtyOutputBuffer()
let reference: string | undefined
for (let i = 0; i < 200; i++) {
const chunk = `${i}:${'p'.repeat(700)}\n`
buffer.append(chunk)
reference = referenceAppend(reference, chunk)
}
buffer.compact()
expect(buffer.retainedChunks().chunks).toEqual([reference])
expect(buffer.read()).toBe(reference)
// After compact, a read re-collapses fragmentation from later appends.
buffer.append('after')
reference = referenceAppend(reference, 'after')
expect(buffer.read()).toBe(reference)
expect(buffer.retainedChunks().chunks).toEqual([reference])
})
it('retainedChunks window-trimmed join always equals read()', () => {
const rng = mulberry32(0xfeed)
const buffer = new RecentPtyOutputBuffer()
const joinRetained = (): string => {
const { chunks } = buffer.retainedChunks()
const joined = chunks.join('')
return joined.slice(Math.max(0, joined.length - RECENT_PTY_OUTPUT_LIMIT))
}
for (let i = 0; i < 300; i++) {
buffer.append(String.fromCharCode(33 + (i % 90)).repeat(Math.floor(rng() * 1500)))
if (i % 11 === 0) {
expect(joinRetained()).toBe(buffer.read())
}
}
expect(joinRetained()).toBe(buffer.read())
})
it('stays equivalent under randomized chunk sizes straddling the cap', () => {
const rng = mulberry32(0xc0ffee)
for (let round = 0; round < 5; round++) {
+56 -5
View File
@@ -14,20 +14,33 @@ const DROPPED_HEAD_COMPACT_THRESHOLD = 1024
export class RecentPtyOutputBuffer {
private chunks: string[] = []
private headIndex = 0
// Code units already trimmed off the front of the head chunk. Deferred to
// read() so repeated small trims never allocate a substring per append.
// Code units already trimmed off the front of the head chunk. Deferred so
// repeated small trims never allocate a substring per append, and so the
// head chunk's original text stays available for candidate backfill.
private headOffset = 0
private totalLen = 0
// True when the stored head chunk is not the full original PTY chunk (a
// single over-limit append is stored pre-sliced), so backfill replay knows
// the original line context of its leading text is gone.
private headChunkIsPartial = false
// Original chunk boundaries are owed only to the one-time path-candidate
// backfill; compact() ends that obligation and lets read() collapse.
private preserveChunkBoundaries: boolean
constructor(options?: { preserveChunkBoundaries?: boolean }) {
this.preserveChunkBoundaries = options?.preserveChunkBoundaries ?? true
}
append(data: string): void {
if (data.length === 0) {
return
}
if (data.length >= RECENT_PTY_OUTPUT_LIMIT) {
this.chunks = [data.slice(-RECENT_PTY_OUTPUT_LIMIT)]
this.headIndex = 0
this.headOffset = 0
this.totalLen = RECENT_PTY_OUTPUT_LIMIT
return
}
if (data.length === 0) {
this.headChunkIsPartial = data.length > RECENT_PTY_OUTPUT_LIMIT
return
}
this.chunks.push(data)
@@ -40,6 +53,7 @@ export class RecentPtyOutputBuffer {
this.chunks[this.headIndex] = ''
this.headIndex += 1
this.headOffset = 0
this.headChunkIsPartial = false
this.totalLen -= headRemaining
} else {
this.headOffset += excess
@@ -53,6 +67,19 @@ export class RecentPtyOutputBuffer {
}
read(): string {
if (this.preserveChunkBoundaries) {
// Join without mutating: boundaries and the original head chunk are
// still owed to retainedChunks(); reads are rare before compact().
if (this.chunks.length - this.headIndex > 1) {
const retained = this.chunks.slice(this.headIndex)
if (this.headOffset > 0) {
retained[0] = retained[0].slice(this.headOffset)
}
return retained.join('')
}
const head = this.chunks[this.headIndex] ?? ''
return this.headOffset > 0 ? head.slice(this.headOffset) : head
}
if (this.chunks.length - this.headIndex > 1) {
// Collapse to the joined tail so repeated reads stay O(1).
const retained = this.chunks.slice(this.headIndex)
@@ -69,4 +96,28 @@ export class RecentPtyOutputBuffer {
}
return this.chunks[this.headIndex] ?? ''
}
/**
* Retained chunks with original PTY boundaries. The head chunk is its full
* original text (any window-trimmed prefix included) unless
* headChunkIsPartial. Why: path-candidate backfill must replay the eager
* per-chunk extraction exactly — trimming or joining chunks changes the
* candidate set. Only meaningful before compact().
*/
retainedChunks(): { chunks: string[]; headChunkIsPartial: boolean } {
return {
chunks: this.chunks.slice(this.headIndex),
headChunkIsPartial: this.headChunkIsPartial
}
}
/**
* Ends the chunk-boundary obligation after the one-time backfill and
* collapses immediately, so the append/read hot path returns to the
* compact single-chunk steady state.
*/
compact(): void {
this.preserveChunkBoundaries = false
this.read()
}
}
+157
View File
@@ -1,5 +1,6 @@
/* eslint-disable max-lines -- Why: this integration-style RPC test keeps the request/response contract together so regressions in the external CLI surface are easier to spot. */
import { existsSync, mkdtempSync } from 'node:fs'
import { rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createConnection, type Socket } from 'node:net'
@@ -3273,6 +3274,162 @@ describe('OrcaRuntimeRpcServer', () => {
}
})
it('authorizes a mobile artifact tap after first-connect backfill even once the raw window scrolls', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
const runtime = new OrcaRuntimeService(makeStore() as never)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getCwd: async () => '/tmp/worktree-a',
getForegroundProcess: async () => null
})
const server = new OrcaRuntimeRpcServer({
runtime,
userDataPath,
enableWebSocket: true,
wsPort: 0
})
// Real artifact under the temp root so the grant path stats it.
const artifactPath = join(tmpdir(), `orca-artifact-${process.pid}-${Date.now()}.json`)
await writeFile(artifactPath, '{"ok":true}')
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Agent',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
// Path printed before any mobile client exists: tracking is inactive, so
// only the retained raw window knows it at connect time.
runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100)
await server.start()
const offer = server.createPairingOffer({
address: '127.0.0.1',
name: 'phone',
scope: 'mobile'
})
expect(offer.available).toBe(true)
if (!offer.available) {
throw new Error('WebSocket pairing unavailable')
}
// Full direct E2EE authentication drives MobileSocketWiring.onReady (the
// relay transport attaches through the same wiring), which must backfill
// candidates from the raw window without any direct activation call.
const phone = await authenticateMobileWsSession(offer.pairingUrl)
const phoneResponses = createEncryptedWsResponseReader(phone)
try {
// Post-connect pathless output scrolls the artifact out of the raw
// 64KiB window; only the connect-time backfilled candidate can answer.
runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200)
sendEncryptedWsRequest(phone, {
id: 'phone_terminals',
method: 'terminal.list',
params: { worktree: 'id:repo-1::/tmp/worktree-a' }
})
const listResponse = await phoneResponses.next('phone_terminals')
const handle = (listResponse.result as { terminals: { handle: string }[] }).terminals[0]!
.handle
expect(handle).toBeTruthy()
sendEncryptedWsRequest(phone, {
id: 'phone_tap',
method: 'files.resolveTerminalPath',
params: {
worktree: 'id:repo-1::/tmp/worktree-a',
pathText: artifactPath,
terminal: handle
}
})
await expect(phoneResponses.next('phone_tap')).resolves.toMatchObject({
ok: true,
result: {
exists: true,
isDirectory: false,
openTarget: {
kind: 'absolute-file',
provider: 'local',
grantId: expect.any(String)
}
}
})
} finally {
phoneResponses.dispose()
phone.ws.close()
await server.stop()
await rm(artifactPath, { force: true })
}
})
it('completes remote E2EE authentication against a runtime proxy without activateRecentPtyPathCandidateTracking', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
// Why: a remote-host runtime proxy only implements RPC-forwarded methods;
// activation is a local-host concern, so the proxy legitimately lacks
// activateRecentPtyPathCandidateTracking and onReady must not throw.
const runtimeProxy = {
getRuntimeId: () => 'proxy-runtime-test',
getStartedAt: () => 1,
getStatus: () => ({ graphStatus: 'unavailable' }),
cleanupSubscriptionsForConnection: () => {},
cancelMobileDictationForConnection: () => {},
onClientDisconnected: () => {}
} as unknown as OrcaRuntimeService
expect(
(runtimeProxy as { activateRecentPtyPathCandidateTracking?: unknown })
.activateRecentPtyPathCandidateTracking
).toBeUndefined()
const server = new OrcaRuntimeRpcServer({
runtime: runtimeProxy,
userDataPath,
enableWebSocket: true,
wsPort: 0
})
await server.start()
const offer = server.createPairingOffer({
address: '127.0.0.1',
name: 'remote',
scope: 'runtime'
})
expect(offer.available).toBe(true)
if (!offer.available) {
throw new Error('WebSocket pairing unavailable')
}
// Real E2EE pairing + authentication drives MobileSocketWiring.onReady
// before e2ee_authenticated is sent; a throwing onReady never authenticates.
const session = await authenticateMobileWsSession(offer.pairingUrl)
const responses = createEncryptedWsResponseReader(session)
try {
sendEncryptedWsRequest(session, { id: 'proxy_status', method: 'status.get' })
await expect(responses.next('proxy_status')).resolves.toMatchObject({
id: 'proxy_status',
ok: true,
result: { graphStatus: 'unavailable' }
})
} finally {
responses.dispose()
session.ws.close()
await server.stop()
}
})
it('keeps active runtime multiplex streams responsive while a background stream is ACK-limited over WebSocket', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
const writes: { terminal: string; text: string }[] = []
+9 -1
View File
@@ -851,7 +851,15 @@ export class OrcaRuntimeRpcServer {
)
},
onBinary: (socket, bytes) => this.handleWebSocketBinaryMessage(bytes, socket.ws),
onReady: () => this.mobileRelayPairingProvider?.onDemandStateChanged?.(),
onReady: () => {
// Why: first authenticated mobile/remote client (direct WS and
// cloud relay both attach here) starts path-candidate tracking.
// Activation is a local-host concern: candidate buffers live on the
// buffer-owning host's runtime, so a remote runtime proxy may
// legitimately lack this method (its own server activates it).
this.runtime.activateRecentPtyPathCandidateTracking?.()
this.mobileRelayPairingProvider?.onDemandStateChanged?.()
},
onClose: (socket, hasOtherConnections) => {
if (!socket) {
return