Merge remote-tracking branch 'origin/main' into fix/renderer-crash-white-screen

# Conflicts:
#	config/reliability-gates.jsonc
#	src/main/runtime/orca-runtime.ts
This commit is contained in:
Brennan Benson
2026-07-13 20:06:10 -07:00
42 changed files with 1981 additions and 206 deletions
@@ -305,6 +305,25 @@ describe('SshChannelMultiplexer', () => {
expect(() => vi.advanceTimersByTime(5_000)).not.toThrow()
expect(mux.isDisposed()).toBe(true)
})
it('drives keepalive sends AND dead-link detection from a single interval', () => {
// The keepalive send and the liveness/timeout check were merged from two
// 5s intervals into one; there must be exactly one recurring timer.
expect(vi.getTimerCount()).toBe(1)
// Send half: a keepalive is written on the tick.
const before = transport.written.length
vi.advanceTimersByTime(5_000)
expect(transport.written.length).toBeGreaterThan(before)
expect(transport.written.at(-1)![0]).toBe(MessageType.KeepAlive)
expect(vi.getTimerCount()).toBe(1)
// Check half: with no inbound frames or acks, the same interval declares
// the link dead (no-data + oldest-unacked both exceed the 20s window).
expect(mux.isDisposed()).toBe(false)
vi.advanceTimersByTime(25_000)
expect(mux.isDisposed()).toBe(true)
})
})
describe('wake guard (timer pause across system sleep, #7773)', () => {
+15 -19
View File
@@ -54,8 +54,7 @@ export class SshChannelMultiplexer {
// generic notification listener that already serves fs.changed.
private methodNotificationHandlers = new Map<string, Set<MethodNotificationHandler>>()
private disposeHandlers: ((reason: 'shutdown' | 'connection_lost') => void)[] = []
private keepaliveTimer: ReturnType<typeof setInterval> | null = null
private timeoutTimer: ReturnType<typeof setInterval> | null = null
private connectionHealthTimer: ReturnType<typeof setInterval> | null = null
private disposed = false
// Track the oldest unacked outgoing message timestamp
@@ -88,8 +87,7 @@ export class SshChannelMultiplexer {
if (this.disposed) {
return
}
this.startKeepalive()
this.startTimeoutCheck()
this.startConnectionHealthTimer()
}
onNotification(handler: NotificationHandler): () => void {
@@ -276,13 +274,9 @@ export class SshChannelMultiplexer {
}
this.disposed = true
if (this.keepaliveTimer) {
clearInterval(this.keepaliveTimer)
this.keepaliveTimer = null
}
if (this.timeoutTimer) {
clearInterval(this.timeoutTimer)
this.timeoutTimer = null
if (this.connectionHealthTimer) {
clearInterval(this.connectionHealthTimer)
this.connectionHealthTimer = null
}
// Why: the renderer uses the error code to distinguish temporary disconnects
@@ -488,15 +482,17 @@ export class SshChannelMultiplexer {
}
}
private startKeepalive(): void {
this.keepaliveTimer = setInterval(() => {
this.sendKeepAlive()
}, KEEPALIVE_SEND_MS)
}
private startTimeoutCheck(): void {
// Why: one 5s interval owns BOTH the periodic keepalive send and the
// liveness/dead-link check. They used to be two separate 5s intervals that
// always fired back-to-back (keepalive created first); folding them into one
// tick — send first (as the keepalive timer did), then run the check (as the
// timeout timer did) — halves the per-connection timer count with identical
// behavior, including the #7773 wake-gap recovery below.
private startConnectionHealthTimer(): void {
let lastTickAt = Date.now()
this.timeoutTimer = setInterval(() => {
this.connectionHealthTimer = setInterval(() => {
this.sendKeepAlive()
if (this.disposed) {
return
}
+1
View File
@@ -34,6 +34,7 @@ function formatStatusResult(status: CliStatusResult): { stdout: string; stderr:
stdout: `${[
`appRunning: ${status.app.running}`,
`pid: ${status.app.pid ?? 'none'}`,
`desktopWindowStatus: ${status.app.desktopWindowStatus ?? 'unknown'}`,
`runtimeState: ${status.runtime.state}`,
`runtimeReachable: ${status.runtime.reachable}`,
`runtimeId: ${status.runtime.runtimeId ?? 'none'}`,
+2 -1
View File
@@ -232,6 +232,7 @@ export function createRemoteCliInstallPlan(env: RemoteCliInstallEnv): RemoteCliI
].join('\n')
}
],
postWriteCommands: [`chmod +x ${quoteSh(launcherPath)} 2>/dev/null; true`]
// Surface chmod failures: a non-executable launcher must fail install loudly, not silently.
postWriteCommands: [`chmod +x ${quoteSh(launcherPath)}`]
}
}
+5 -1
View File
@@ -158,7 +158,11 @@ async function dispatchRemoteCli(
}
const status = response.result as RuntimeStatus
const cliStatus: CliStatusResult = {
app: { running: true, pid: null },
app: {
running: true,
pid: null,
...(status.desktopWindowStatus ? { desktopWindowStatus: status.desktopWindowStatus } : {})
},
runtime: {
state: status.graphStatus === 'ready' ? 'ready' : 'graph_not_ready',
reachable: true,