mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
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:
@@ -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)', () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'}`,
|
||||
|
||||
@@ -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)}`]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user