fix(push): honor "only when desktop is away" on headless orca serve (#31956)

* fix(push): decide orca serve's desktop-away from attached desktops (#31570)

* fix(push): report away on sleep and lock, guard late reports and prune stale presence

* chore: justify the presence-reporter state slot widening

* fix(push): hold away from suspend or lock until resume or unlock

* fix(push): key presence sockets by connection instance so a replaced connection reports at once
This commit is contained in:
Neil
2026-10-22 03:36:10 -07:00
committed by GitHub
parent abc0c743a2
commit a3fa44ad07
18 changed files with 423 additions and 18 deletions
@@ -313,7 +313,7 @@ function getSharedControlConnection(
sharedControlConnections.set(environmentId, cached)
const entry = cached
// Why ready is enough: a socket reaches ready only on its own server frame, which stamps it.
setSharedControlAnswerSource(environmentId, () =>
setSharedControlAnswerSource(environmentId, entry.connection, () =>
entry.connection.getDiagnostics().state === 'ready' ? entry.answeredAt() : null
)
}
@@ -1,14 +1,33 @@
/**
* When each environment's ready control socket last heard from its server. A leaf module: the
* serving gate reads it without importing the connection cache, which imports the gate back.
* Each environment's control socket and when it last heard from its server while ready. A leaf
* module: the serving gate reads it without importing the connection cache, which imports the gate back.
*/
const sources = new Map<string, () => number | null>()
export type SharedControlSocket = {
isReady(): boolean
readonly socketGeneration: { peek(): number }
request(method: string, params: unknown, timeoutMs: number): Promise<unknown>
}
/** A ready control socket; `socket` changes whenever it reconnects or the connection is replaced. */
export type ReadySharedControlSocket = {
environmentId: string
socket: string
request: SharedControlSocket['request']
}
const sources = new Map<
string,
{ connection: SharedControlSocket; answeredAt: () => number | null; instance: number }
>()
// Why: a replacement connection restarts its socket generation, so the key needs the instance too.
let nextInstance = 0
export function setSharedControlAnswerSource(
environmentId: string,
source: () => number | null
connection: SharedControlSocket,
answeredAt: () => number | null
): void {
sources.set(environmentId, source)
sources.set(environmentId, { connection, answeredAt, instance: ++nextInstance })
}
export function clearSharedControlAnswerSource(environmentId: string): void {
@@ -17,5 +36,21 @@ export function clearSharedControlAnswerSource(environmentId: string): void {
/** Null while there is no ready control socket for the environment. */
export function getRemoteRuntimeSharedControlAnsweredAt(environmentId: string): number | null {
return sources.get(environmentId)?.() ?? null
return sources.get(environmentId)?.answeredAt() ?? null
}
/** Every ready control socket, without opening one. */
export function listReadySharedControlSockets(): ReadySharedControlSocket[] {
return [...sources.entries()].flatMap(([environmentId, { connection, instance }]) =>
connection.isReady()
? [
{
environmentId,
socket: `${instance}:${connection.socketGeneration.peek()}`,
request: (method: string, params: unknown, timeoutMs: number) =>
connection.request(method, params, timeoutMs)
}
]
: []
)
}
@@ -0,0 +1,82 @@
import { expect, it, vi } from 'vitest'
import { DESKTOP_PRESENCE_REPORT_INTERVAL_MS } from '../../shared/desktop-client-presence'
import {
clearSharedControlAnswerSource,
listReadySharedControlSockets,
setSharedControlAnswerSource
} from '../ipc/runtime-environments/runtime-environment-shared-control-answers'
import { createDesktopPresenceReporter } from './desktop-presence-reporter'
it('reports on change, on a new socket and as a keepalive, and never reports unknown as present', async () => {
let away: boolean | undefined = false
let now = 0
let socket = '1'
const request = vi.fn(async (_method: string, _params: unknown, _timeoutMs: number) => ({
ok: true
}))
const listTargets = vi.fn(() => [{ environmentId: 'ssh-host', socket, request }])
const reporter = createDesktopPresenceReporter({
readAway: () => away,
listTargets,
now: () => now
})
const tick = reporter.tick
const reported = () => request.mock.calls.map((call) => call[1])
tick()
tick()
expect(listTargets).toHaveBeenCalledWith('notifications.desktop-presence.v1')
expect(reported()).toEqual([{ away: false }])
away = undefined
tick()
expect(reported()).toEqual([{ away: false }, { away: true }])
now += DESKTOP_PRESENCE_REPORT_INTERVAL_MS
tick()
expect(reported()).toHaveLength(3)
away = false
socket = '2'
now += 1
tick()
socket = '3'
tick()
expect(reported()).toEqual([
{ away: false },
{ away: true },
{ away: true },
{ away: false },
{ away: false }
])
request.mockRejectedValueOnce(new Error('socket closed'))
socket = '4'
tick()
await Promise.resolve()
await Promise.resolve()
tick()
expect(reported()).toHaveLength(7)
// Suspend reports away at once and holds it while the idle clock still reads active.
reporter.setForcedAway(true)
expect(reported().at(-1)).toEqual({ away: true })
now += DESKTOP_PRESENCE_REPORT_INTERVAL_MS
tick()
expect(reported().at(-1)).toEqual({ away: true })
reporter.setForcedAway(false)
expect(reported().at(-1)).toEqual({ away: false })
})
it('keys a replaced connection apart from the one it replaced, though both start at generation 1', () => {
const connection = () => ({
isReady: () => true,
socketGeneration: { peek: () => 1 },
request: async () => ({ ok: true })
})
try {
setSharedControlAnswerSource('replaced-host', connection(), () => null)
const [before] = listReadySharedControlSockets()
setSharedControlAnswerSource('replaced-host', connection(), () => null)
const [after] = listReadySharedControlSockets()
expect(after.socket).not.toBe(before.socket)
} finally {
clearSharedControlAnswerSource('replaced-host')
}
})
@@ -0,0 +1,113 @@
import {
DESKTOP_PRESENCE_REPORT_INTERVAL_MS,
NOTIFICATIONS_DESKTOP_PRESENCE_RUNTIME_CAPABILITY
} from '../../shared/desktop-client-presence'
import { getRuntimeEnvironmentStatusSnapshots } from '../ipc/runtime-environments/runtime-environment-request-connections'
import {
listReadySharedControlSockets,
type ReadySharedControlSocket
} from '../ipc/runtime-environments/runtime-environment-shared-control-answers'
/** How often local idle state is sampled, so a return to the desk reaches hosts within seconds. */
const SAMPLE_INTERVAL_MS = 5_000
const REPORT_TIMEOUT_MS = 10_000
type Sent = { socket: string; away: boolean; at: number }
export type DesktopPresenceReporterDeps = {
readAway: () => boolean | undefined
listTargets: (capability: string) => ReadySharedControlSocket[]
now?: () => number
}
/**
* Tells each connected host whether someone is at this desktop, so a headless host can honor a
* phone's "only when desktop is away" (#31570). Sent on change, on a new socket, and as a keepalive.
*/
export type DesktopPresenceReporter = {
tick(): void
/** Latched across ticks, so a sample between suspend and sleep cannot report present. */
setForcedAway(forced: boolean): void
}
export function createDesktopPresenceReporter(
deps: DesktopPresenceReporterDeps
): DesktopPresenceReporter {
const now = deps.now ?? Date.now
const sent = new Map<string, Sent>()
let forcedAway = false
const tick = (): void => {
const targets = deps.listTargets(NOTIFICATIONS_DESKTOP_PRESENCE_RUNTIME_CAPABILITY)
if (targets.length === 0) {
sent.clear()
return
}
// Why: unknown local presence must never silence a phone, so only a proven "active" is present.
const away = forcedAway || deps.readAway() !== false
const at = now()
const live = new Set(targets.map((target) => target.environmentId))
for (const environmentId of sent.keys()) {
if (!live.has(environmentId)) {
sent.delete(environmentId)
}
}
for (const target of targets) {
const last = sent.get(target.environmentId)
if (
last &&
last.socket === target.socket &&
last.away === away &&
at - last.at < DESKTOP_PRESENCE_REPORT_INTERVAL_MS
) {
continue
}
const entry = { socket: target.socket, away, at }
sent.set(target.environmentId, entry)
// Why: a refused report waits for the keepalive; only an unsent one retries on the next tick.
void target
.request('notifications.reportDesktopPresence', { away }, REPORT_TIMEOUT_MS)
.catch(() => {
if (sent.get(target.environmentId) === entry) {
sent.delete(target.environmentId)
}
})
}
}
return {
tick,
setForcedAway: (forced) => {
forcedAway = forced
tick()
}
}
}
/** Ready control sockets whose host's last status advertised `capability`. */
export function listCapableSharedControlSockets(capability: string): ReadySharedControlSocket[] {
const capable = new Set(
getRuntimeEnvironmentStatusSnapshots()
.filter((snapshot) => snapshot.status?.capabilities?.includes(capability))
.map((snapshot) => snapshot.environmentId)
)
return listReadySharedControlSockets().filter((target) => capable.has(target.environmentId))
}
export function startDesktopPresenceReporter(
deps: DesktopPresenceReporterDeps & {
/** Sleep and screen lock report away at once and hold it until resume or unlock. */
subscribePowerEvents: (handlers: { away: () => void; back: () => void }) => () => void
}
): () => void {
const reporter = createDesktopPresenceReporter(deps)
const timer = setInterval(reporter.tick, SAMPLE_INTERVAL_MS)
// Why: presence reporting must never hold the app open at quit.
timer.unref?.()
const unsubscribe = deps.subscribePowerEvents({
away: () => reporter.setForcedAway(true),
back: () => reporter.setForcedAway(false)
})
return () => {
clearInterval(timer)
unsubscribe()
}
}
@@ -0,0 +1,32 @@
import { DESKTOP_PRESENCE_TTL_MS } from '../../shared/desktop-client-presence'
/** The last presence each attached desktop connection reported to this host. */
export class DesktopClientPresenceTracker {
private readonly reports = new Map<string, { away: boolean; at: number }>()
has(connectionId: string): boolean {
return this.reports.has(connectionId)
}
report(connectionId: string, away: boolean, now: number): void {
this.reports.set(connectionId, { away, at: now })
}
forget(connectionId: string): void {
this.reports.delete(connectionId)
}
/** True only on a fresh "present" report; silence, staleness and older desktops read as away. */
anyPresent(now: number): boolean {
let present = false
for (const [connectionId, { away, at }] of this.reports) {
if (now - at >= DESKTOP_PRESENCE_TTL_MS) {
// Why prune: the next report re-registers the forget-on-close, so nothing can pile up.
this.reports.delete(connectionId)
} else if (!away) {
present = true
}
}
return present
}
}
@@ -6,6 +6,9 @@ import { readDesktopAwayState } from '../../notifications/desktop-away-state'
import { DeviceRegistry } from '../device-registry'
import { RuntimeMobileNotificationController } from '../runtime-mobile-notification-controller'
import { setRuntimeDesktopSurface } from '../runtime-desktop-surface'
import { RuntimeSubscriptionRegistry } from '../runtime-subscription-registry'
import { reportDesktopPresence } from '../rpc/methods/notifications'
import { DESKTOP_PRESENCE_TTL_MS } from '../../../shared/desktop-client-presence'
import { DesktopPushService } from './desktop-push-service'
import { PushUnregisterOutbox } from './push-unregister-outbox'
import { createPushHostKeypair } from './push-host-challenge-fixtures'
@@ -103,15 +106,55 @@ it('carries the native idle boundary through replay and push dispatch', async ()
)
})
it('keeps headless presence unknown and legacy socket events readable', async () => {
it('decides headless away from attached desktops, so the away-only filter holds on orca serve', async () => {
const now = 1_800_000_000_000
const clock = vi.spyOn(Date, 'now').mockReturnValue(now)
setRuntimeDesktopSurface(null)
const h = await pipeline()
const events: unknown[] = []
h.controller.onDispatched((event) => events.push(JSON.parse(JSON.stringify(event))))
h.dispatch()
await flush()
expect(events[0]).not.toHaveProperty('desktopAway')
expect(h.client.send).toHaveBeenCalledTimes(1)
const subscriptions = new RuntimeSubscriptionRegistry()
const runtime = {
reportDesktopClientPresence: h.controller.reportDesktopClientPresence.bind(h.controller),
registerSubscriptionCleanup: subscriptions.register.bind(subscriptions)
}
const desktop: {
runtime: typeof runtime
clientKind?: 'mobile' | 'runtime'
connectionId: string
} = { runtime, clientKind: 'runtime', connectionId: 'desktop-socket' }
const report = (
away: boolean,
context: Partial<typeof desktop> & { signal?: AbortSignal } = {}
) => reportDesktopPresence({ away }, { ...desktop, ...context })
const sends = async () => {
h.dispatch()
await flush()
return h.client.send.mock.calls.length
}
// No desktop has ever reported: nobody is at one.
expect(await sends()).toBe(1)
// Neither a phone nor a socket that already closed can vouch for a desktop.
expect(report(false, { clientKind: 'mobile' })).toEqual({ recorded: false })
expect(report(false, { signal: AbortSignal.abort() })).toEqual({ recorded: false })
expect(await sends()).toBe(2)
report(false)
expect(await sends()).toBe(2)
report(true)
expect(await sends()).toBe(3)
report(false)
expect(await sends()).toBe(3)
// A desktop that stops reporting (or an older one that never did) lapses to away.
clock.mockReturnValue(now + DESKTOP_PRESENCE_TTL_MS)
expect(await sends()).toBe(4)
report(false)
expect(await sends()).toBe(4)
subscriptions.cleanupForConnection('desktop-socket')
expect(await sends()).toBe(5)
expect(
h.controller
.getMissedSince(0)
.map((event) => (event.type === 'notification' ? event.desktopAway : null))
).toEqual([true, true, false, true, false, true, false, true])
})
it('expires persisted registration at seven days despite host activity and renews explicitly', async () => {
+28 -1
View File
@@ -1,8 +1,10 @@
import { createNotificationStreamFilter } from './notification-stream-policy'
import { defineStreamingMethod, defineMethod } from '../core'
import { defineStreamingMethod, defineMethod, type RpcContext } from '../core'
import type { OrcaRuntimeService } from '../../orca-runtime'
import {
NotificationGetMissedSinceParams,
NotificationRegisterPushParams,
NotificationReportDesktopPresenceParams,
NotificationUnsubscribeParams,
NotificationsSubscribeParams
} from '../../../../shared/rpc-contract/notifications-params'
@@ -12,6 +14,25 @@ import {
// notifications.subscribe calls landed on the same millisecond.
let notificationsSubscriptionSeq = 0
type DesktopPresenceReportContext = Pick<RpcContext, 'clientKind' | 'connectionId' | 'signal'> & {
runtime: Pick<OrcaRuntimeService, 'reportDesktopClientPresence' | 'registerSubscriptionCleanup'>
}
/** Only a paired desktop's own socket speaks for a desktop; the report lives as long as it. */
export function reportDesktopPresence(
params: { away: boolean },
{ runtime, clientKind, connectionId, signal }: DesktopPresenceReportContext
): { recorded: boolean } {
// A closed socket already ran its cleanups, so a late report would never be forgotten.
if (clientKind !== 'runtime' || !connectionId || signal?.aborted) {
return { recorded: false }
}
runtime.reportDesktopClientPresence(connectionId, params.away, (forget) =>
runtime.registerSubscriptionCleanup(`desktop-presence-${connectionId}`, forget, connectionId)
)
return { recorded: true }
}
// Legacy callers retain filtered socket alerts; push clients opt into the full event stream.
export const NOTIFICATION_METHODS = [
defineStreamingMethod({
@@ -76,6 +97,12 @@ export const NOTIFICATION_METHODS = [
}
}
}),
defineMethod({
name: 'notifications.reportDesktopPresence',
permission: 'workspace',
params: NotificationReportDesktopPresenceParams,
handler: async (params, context) => reportDesktopPresence(params, context)
}),
defineMethod({
name: 'notifications.registerPush',
permission: 'pairing-admin',
+2 -1
View File
@@ -18,8 +18,9 @@ import type { BrowserWindow, IpcMainEvent } from 'electron'
export type RuntimeDesktopSurface = {
/** Headless hosts retain the formatter's English defaults. */
translateNotification?(key: string, fallback: string): string
/** Show a native notification. Returns false when the host cannot, so callers can say so. */
/** This machine's own idle state; undefined is unknown. Absent on hosts with no desktop. */
isAwayForMobileNotifications?(): boolean | undefined
/** Show a native notification. Returns false when the host cannot, so callers can say so. */
showNotification(input: { title: string; body: string }): boolean
/** The renderer window with this id, or null when there is no desktop. */
findWindowById(id: number): BrowserWindow | null
@@ -16,6 +16,7 @@ import { MobileNotificationReplayBuffer } from './mobile-notification-replay'
import { notifyRuntimeListeners } from './runtime-async-boundaries'
import { getRuntimeDesktopSurface } from './runtime-desktop-surface'
import { MobileNotificationDismissalStore } from './mobile-notification-dismissal-store'
import { DesktopClientPresenceTracker } from './desktop-client-presence-tracker'
import type { DeliveredNotificationIdentity } from '../../shared/mobile-notification-identity'
export type MobileNotificationDispatchEvent = {
@@ -66,6 +67,7 @@ export class RuntimeMobileNotificationController {
private readonly replay = new MobileNotificationReplayBuffer()
private pushRegistrar: MobilePushRegistrar | null = null
private dismissalStore: MobileNotificationDismissalStore | null = null
private readonly desktopClients = new DesktopClientPresenceTracker()
configureDismissalStore(userDataPath: string): void {
this.dismissalStore = new MobileNotificationDismissalStore(userDataPath)
@@ -107,6 +109,29 @@ export class RuntimeMobileNotificationController {
return (await this.pushRegistrar?.unregister(deviceId)) ?? { unregistered: false }
}
/** `bindForget` runs for a connection without a live report, before it is recorded. */
reportDesktopClientPresence(
connectionId: string,
away: boolean,
bindForget: (forget: () => void) => void
): void {
if (!this.desktopClients.has(connectionId)) {
bindForget(() => this.desktopClients.forget(connectionId))
}
this.desktopClients.report(connectionId, away, Date.now())
}
// Why: undefined means unknown and must never silence a phone; only a fresh report or the local
// idle state proves someone is at a desktop. A host with no desktop of its own (orca serve) has
// only its attached desktops to ask, so their silence is away rather than unknown (#31570).
private resolveDesktopAway(): boolean | undefined {
if (this.desktopClients.anyPresent(Date.now())) {
return false
}
const surface = getRuntimeDesktopSurface()
return surface.isAwayForMobileNotifications ? surface.isAwayForMobileNotifications() : true
}
onDispatched(listener: (event: MobileNotificationEvent) => void): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
@@ -130,7 +155,7 @@ export class RuntimeMobileNotificationController {
event = {
...event,
legacySocketAllowed,
desktopAway: getRuntimeDesktopSurface().isAwayForMobileNotifications?.()
desktopAway: this.resolveDesktopAway()
}
}
const seq = this.replay.record(event)
@@ -30,6 +30,7 @@ export type RuntimeServiceCommandSurface = {
onNotificationDispatched: RuntimeMobileNotificationController['onDispatched']
getMobileNotificationListenerCount: RuntimeMobileNotificationController['getListenerCount']
dispatchMobileNotification: RuntimeMobileNotificationController['dispatch']
reportDesktopClientPresence: RuntimeMobileNotificationController['reportDesktopClientPresence']
getMissedNotificationsSince: RuntimeMobileNotificationController['getMissedSince']
configureNotificationDismissalStore: RuntimeMobileNotificationController['configureDismissalStore']
closeNotificationDismissals: RuntimeMobileNotificationController['closeDismissals']
@@ -138,6 +139,7 @@ export function installRuntimeServiceCommandSurface(
onNotificationDispatched: notifications.onDispatched.bind(notifications),
getMobileNotificationListenerCount: notifications.getListenerCount.bind(notifications),
dispatchMobileNotification: notifications.dispatch.bind(notifications),
reportDesktopClientPresence: notifications.reportDesktopClientPresence.bind(notifications),
getMissedNotificationsSince: notifications.getMissedSince.bind(notifications),
configureNotificationDismissalStore: notifications.configureDismissalStore.bind(notifications),
closeNotificationDismissals: notifications.closeDismissals.bind(notifications),
@@ -37,6 +37,10 @@ vi.mock('../agent-awake-service', () => ({
vi.mock('../system-resume-broadcast', () => ({
registerSystemResumeBroadcast: () => () => {}
}))
vi.mock('../notifications/desktop-presence-reporter', () => ({
listCapableSharedControlSockets: () => [],
startDesktopPresenceReporter: () => () => {}
}))
vi.mock('../agent-hooks/server', () => ({
agentHookServer: {
subscribeStatusChanges: () => () => {},
+22 -1
View File
@@ -1,8 +1,13 @@
import { app } from 'electron'
import { app, powerMonitor } from 'electron'
import { join } from 'node:path'
import { AgentAwakeService } from '../agent-awake-service'
import { normalizeComputerAwakeMode } from '../../shared/computer-awake-mode'
import { registerSystemResumeBroadcast } from '../system-resume-broadcast'
import { readDesktopAwayState } from '../notifications/desktop-away-state'
import {
listCapableSharedControlSockets,
startDesktopPresenceReporter
} from '../notifications/desktop-presence-reporter'
import { agentHookServer } from '../agent-hooks/server'
import { installHookStatusSessionTabsRepublish } from '../agent-hooks/hook-status-session-tabs-republish'
import { initTelemetry, track } from '../telemetry/client'
@@ -29,6 +34,22 @@ export function initializeMainProcessObservers(): void {
throw new Error('Store must be initialized before observers')
}
state.unsubscribeSystemResumeBroadcast = registerSystemResumeBroadcast()
state.stopDesktopPresenceReporter = startDesktopPresenceReporter({
readAway: () => readDesktopAwayState(powerMonitor),
listTargets: listCapableSharedControlSockets,
subscribePowerEvents: ({ away, back }) => {
powerMonitor.on('suspend', away)
powerMonitor.on('lock-screen', away)
powerMonitor.on('resume', back)
powerMonitor.on('unlock-screen', back)
return () => {
powerMonitor.off('suspend', away)
powerMonitor.off('lock-screen', away)
powerMonitor.off('resume', back)
powerMonitor.off('unlock-screen', back)
}
}
})
state.agentAwakeService = new AgentAwakeService()
state.agentAwakeService.setMode(
normalizeComputerAwakeMode(
+2
View File
@@ -120,6 +120,8 @@ function installWillQuitHandler(): void {
state.desktopPushService?.stop()
state.unsubscribeSystemResumeBroadcast?.()
state.unsubscribeSystemResumeBroadcast = null
state.stopDesktopPresenceReporter?.()
state.stopDesktopPresenceReporter = null
// Why: renderer guards can still cancel before this committed phase; `log stream` must survive those vetoes.
stopTccPromptNotice()
const updateQuitInProgress = isQuittingForUpdate()
+2
View File
@@ -103,6 +103,8 @@ export const mainProcessState = {
crashReports: null as CrashReportStore | null,
unsubscribeAgentAwakeStatusChanges: null as (() => void) | null,
unsubscribeSystemResumeBroadcast: null as (() => void) | null,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: widens the null slot to the stop function assigned by main-process-observers.
stopDesktopPresenceReporter: null as (() => void) | null,
watcherShutdownPromise: null as Promise<void> | null,
watcherShutdownDone: false,
automations: null as AutomationService | null,
+10
View File
@@ -0,0 +1,10 @@
// Why: an older host answers notifications.reportDesktopPresence with method-not-found, so a desktop
// reports only to a host advertising this; an older desktop never reports and reads as away.
export const NOTIFICATIONS_DESKTOP_PRESENCE_RUNTIME_CAPABILITY =
'notifications.desktop-presence.v1' as const
/** A desktop re-reports this often while connected, so the host can expire a silent one. */
export const DESKTOP_PRESENCE_REPORT_INTERVAL_MS = 30_000
/** A report older than this no longer counts: three missed reports read as away. */
export const DESKTOP_PRESENCE_TTL_MS = 3 * DESKTOP_PRESENCE_REPORT_INTERVAL_MS
@@ -1,3 +1,4 @@
import { NOTIFICATIONS_DESKTOP_PRESENCE_RUNTIME_CAPABILITY } from './desktop-client-presence'
import { MOBILE_RELAY_RUNTIME_CAPABILITIES } from './mobile-desktop-relay-contract'
// Hosts without this capability have no notifications.registerPush RPC.
@@ -6,5 +7,6 @@ export const NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY = 'notifications.remot
export const MOBILE_RUNTIME_CAPABILITIES = [
'mobile.tasks.v1',
NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY,
NOTIFICATIONS_DESKTOP_PRESENCE_RUNTIME_CAPABILITY,
...MOBILE_RELAY_RUNTIME_CAPABILITIES
] as const
@@ -59,3 +59,5 @@ export const NotificationRegisterPushParams = z
export const NotificationsSubscribeParams = z
.object({ includeDesktopSuppressed: z.boolean().optional() })
.optional()
export const NotificationReportDesktopPresenceParams = z.object({ away: z.boolean() })
+2
View File
@@ -404,6 +404,7 @@ import { NativeChatSession, NativeChatUnsubscribe } from './native-chat-params'
import {
NotificationGetMissedSinceParams,
NotificationRegisterPushParams,
NotificationReportDesktopPresenceParams,
NotificationUnsubscribeParams,
NotificationsSubscribeParams
} from './notifications-params'
@@ -1165,6 +1166,7 @@ export const RPC_PARAMS_BY_METHOD = {
'network.browserTunnel': BrowserNetworkTunnelAttachParams,
'notifications.getMissedSince': NotificationGetMissedSinceParams,
'notifications.registerPush': NotificationRegisterPushParams,
'notifications.reportDesktopPresence': NotificationReportDesktopPresenceParams,
'notifications.subscribe': NotificationsSubscribeParams,
'notifications.testPush': null,
'notifications.unregisterPush': null,