From c582e157f6bc0db6ff2e5734e9da269df5b27a57 Mon Sep 17 00:00:00 2001 From: testikun Date: Wed, 2 Sep 2026 11:16:36 +0800 Subject: [PATCH 1/6] fix(desktop): stop polling unavailable collaboration authority Generated-by: Codex --- ...untime-host-collaboration-ipc-main.test.ts | 1 + .../runtime-host-desktop-manager.test.ts | 14 +++++++ ...me-host-turn-request-inbox-preload.test.ts | 23 ++++++++++- apps/desktop/src/main/runtime-host-boot.ts | 20 +++++++++- apps/desktop/src/main/runtime-host-client.ts | 4 +- .../runtime-host-collaboration-ipc-main.ts | 7 +++- .../src/main/runtime-host-desktop-manager.ts | 40 ++++++++++++++++++- apps/desktop/src/preload/bridge-contract.d.ts | 2 + apps/desktop/src/preload/preload.ts | 24 +++++++++-- .../runtime-host-turn-request-inbox.ts | 11 +++++ .../src/__tests__/protocol.test.ts | 13 ++++++ .../runtime-host/src/protocol/host-status.ts | 12 ++++++ .../runtime-host/src/server/host-kernel.ts | 1 + 13 files changed, 162 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts index 3da977f0a2..b5262a3c69 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts @@ -190,6 +190,7 @@ test('treats an unavailable collaboration authority as an empty background inbox assert.deepEqual(await query({} as Parameters[0]), { canRequestTurns: false, requests: [], + authorityUnavailable: true, }); await assert.rejects( query({} as Parameters[0], 'session-1'), diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts index b2392b6b5b..829d4ef36e 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts @@ -110,6 +110,20 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy assert.equal(second.closeCalls, 1); }); +test('publishes the Runtime Host collaboration capability with its identity', async () => { + const current = candidateHarness(); + (current.candidate.client as unknown as { + status: () => Promise<{ collaborationAuthority: boolean }>; + }).status = async () => ({ collaborationAuthority: false }); + const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, { + startCandidate: async () => ready(current.candidate), + }); + + assert.equal(owner.current()?.collaborationAuthority, false); + assert.equal(owner.entries()[0]?.collaborationAuthority, false); + await owner.close(); +}); + test('quiesces reconnect and waits for the Host process before update install', async () => { const current = candidateHarness({ disconnectOnPrepare: true }); const replacement = candidateHarness(); diff --git a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts index 267504c866..d1e370e112 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts @@ -20,7 +20,28 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; -import { collectAvailablePendingTurnRequests } from '../../preload/runtime-host-turn-request-inbox.js'; +import { + collectAvailablePendingTurnRequests, + selectRuntimeHostCollaborationScopes, +} from '../../preload/runtime-host-turn-request-inbox.js'; + +test('skips an Owner Host that explicitly lacks collaboration authority', () => { + const scopes = selectRuntimeHostCollaborationScopes([ + { hostId: 'local', collaborationAuthority: false }, + { hostId: 'remote', collaborationAuthority: true }, + ]); + + assert.deepEqual(scopes.map(({ hostId }) => hostId), ['remote']); +}); + +test('keeps transiently unavailable collaboration inboxes retryable', async () => { + const requests = await collectAvailablePendingTurnRequests([ + Promise.reject(new Error('connection lost while polling')), + Promise.resolve([request('available', '2026-09-01T00:00:01.000Z')]), + ]); + + assert.deepEqual(requests.map(({ requestId }) => requestId), ['available']); +}); function request(requestId: string, createdAt: string): SessionTurnAccessRequest { return { requestId, createdAt } as SessionTurnAccessRequest; diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 7caee1cf13..5c21ae3b73 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1135,6 +1135,9 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state.target.profile.name, profileKind: state.target.profile.kind, profileAccess, + ...(state.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: state.collaborationAuthority }), ...(hostId ? { hostId } : {}), readiness: state.readiness, isDefault: @@ -1185,6 +1188,9 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state.target.profile.name, profileKind: state.target.profile.kind, profileAccess: runtimeHostProfileAccess(state.target.profile), + ...(state.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: state.collaborationAuthority }), ...(hostId ? { hostId } : {}), readiness: "unavailable", isDefault: @@ -1209,6 +1215,9 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state?.target.profile.name ?? profileId, profileKind: state?.target.profile.kind ?? "remote", profileAccess: state ? runtimeHostProfileAccess(state.target.profile) : "owner", + ...(state?.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: state.collaborationAuthority }), ...(state?.readiness === "ready" ? { hostId: state.candidate.client.hostId } : state?.readiness !== "unavailable" && state && "hostId" in state && state.hostId @@ -1809,6 +1818,7 @@ function registerPersistentClientIpc(): void { target: ResolvedRuntimeHostProfile, readiness: 'ready' | 'reconnecting', hostId: string, + collaborationAuthority?: boolean, ): DesktopRuntimeHostIdentity => ({ hostId, targetEpoch: epoch, @@ -1816,6 +1826,7 @@ function registerPersistentClientIpc(): void { profileName: target.profile.name, profileKind: target.profile.kind, profileAccess: runtimeHostProfileAccess(target.profile), + ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), readiness, }); ipcMain.handle("runtime-host:activeIdentity", () => { @@ -1828,6 +1839,7 @@ function registerPersistentClientIpc(): void { current.target, current.readiness, current.hostId, + current.collaborationAuthority, ); }); ipcMain.handle("runtime-host:identities", () => @@ -1836,7 +1848,13 @@ function registerPersistentClientIpc(): void { const hostId = state.readiness === "ready" ? state.candidate.client.hostId : state.hostId; if (!hostId) return []; return [ - projectRuntimeHostIdentity(state.epoch, state.target, state.readiness, hostId), + projectRuntimeHostIdentity( + state.epoch, + state.target, + state.readiness, + hostId, + state.collaborationAuthority, + ), ]; }), ); diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index f548396cb5..4ae1411d6e 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -302,8 +302,8 @@ export class DesktopRuntimeHostClient { return this.#connectionClosed || this.#closeTask ? 'unavailable' : 'ready'; } - status(): Promise { - return this.connection.status(); + status(timeoutMs?: number): Promise { + return this.connection.status(timeoutMs); } finalizeAccessCredential( diff --git a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts index 502d5b9b23..8898165129 100644 --- a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts @@ -19,6 +19,7 @@ import { RuntimeHostOperationError } from '@maka/runtime-host/client'; import type { DesktopRuntimeHostClient } from './runtime-host-client.js'; +import type { CollaborationTurnRequestQueryResult } from '@maka/runtime-host/protocol'; import { encodeDesktopCollaborationInvitation, type DesktopCollaborationConnectionTarget, @@ -101,7 +102,11 @@ export function registerRuntimeHostCollaborationIpc( return await client.queryCollaborationTurnRequests(requestedSessionId); } catch (error) { if (requestedSessionId === undefined && isCollaborationInboxUnavailable(error)) { - return { canRequestTurns: false, requests: [] }; + return { + canRequestTurns: false, + requests: [], + authorityUnavailable: true, + } satisfies CollaborationTurnRequestQueryResult & { authorityUnavailable: true }; } throw error; } diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts index 36d8eca62d..9f7dc5be20 100644 --- a/apps/desktop/src/main/runtime-host-desktop-manager.ts +++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts @@ -108,6 +108,7 @@ export interface RuntimeHostDesktopTargetSnapshot { readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'ready' | 'reconnecting'; readonly candidate?: DesktopRuntimeHostCandidate; + readonly collaborationAuthority?: boolean; } export type RuntimeHostDesktopTargetState = @@ -116,18 +117,21 @@ export type RuntimeHostDesktopTargetState = readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'connecting' | 'reconnecting'; readonly hostId?: string; + readonly collaborationAuthority?: boolean; } | { readonly epoch: string; readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'ready'; readonly candidate: DesktopRuntimeHostCandidate; + readonly collaborationAuthority?: boolean; } | { readonly epoch: string; readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'unavailable'; readonly hostId?: string; + readonly collaborationAuthority?: boolean; readonly error: Error; }; @@ -219,6 +223,7 @@ interface DesktopRuntimeHostTargetGeneration { readonly observations: RuntimeHostSessionObservationRegistry; state: RuntimeHostDesktopTargetState; hostId?: string; + collaborationAuthority?: boolean; lifecycle?: RuntimeHostReconnectLifecycle; unsubscribeLifecycle?: () => void; unsubscribeRoutes?: () => void; @@ -479,12 +484,20 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { ...(target.hostId ? { hostId: target.hostId } : {}), target: target.target, readiness: candidate ? 'ready' : 'reconnecting', + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), ...(candidate ? { candidate } : {}), }; } entries(): readonly RuntimeHostDesktopTargetState[] { - return [...this.#targets.values()].map((target) => target.state); + return [...this.#targets.values()].map((target) => ({ + ...target.state, + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), + })); } ownsScope(scope: { readonly hostId: string; readonly targetEpoch: string }): boolean { @@ -1085,6 +1098,12 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { // replay one-shot join progress. onConnectionPhase: (phase) => onConnectionPhase?.(phase), ...(refreshPeerRoutes ? {} : { refreshPeerRoutes: false }), + onHostStatus: (status) => { + if (status.collaborationAuthority !== undefined) { + target.collaborationAuthority = status.collaborationAuthority; + } + target.input.onHostStatus?.(status); + }, signal, ...(takeoverHostEpoch === undefined ? {} : { takeoverHostEpoch }), }, @@ -1097,6 +1116,13 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { throw error; } if (result.kind === 'ready') { + const status = result.candidate.client.status; + if (typeof status === 'function') { + const observed = await status.call(result.candidate.client, 5_000).catch(() => undefined); + if (observed?.collaborationAuthority !== undefined) { + target.collaborationAuthority = observed.collaborationAuthority; + } + } target.hostId = result.candidate.client.hostId; const previous = target.lastCandidate; const retainedOwnedProcess = @@ -1353,12 +1379,18 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { target: target.target, readiness: 'ready', candidate, + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), } : { epoch: target.epoch, target: target.target, readiness: 'reconnecting', ...(target.hostId ? { hostId: target.hostId } : {}), + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), }, ); }); @@ -1371,12 +1403,18 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { target: target.target, readiness: 'ready', candidate, + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), } : { epoch: target.epoch, target: target.target, readiness: 'reconnecting', ...(target.hostId ? { hostId: target.hostId } : {}), + ...(target.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: target.collaborationAuthority }), }, ); } diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index 70b940e6b6..3e6b6d0918 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -452,6 +452,7 @@ export interface DesktopRuntimeHostProfileChangedEvent { readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: RuntimeHostProfileAccess; + readonly collaborationAuthority?: boolean; readonly readiness: 'connecting' | 'ready' | 'reconnecting' | 'unavailable'; readonly hostId?: string; readonly isDefault: boolean; @@ -463,6 +464,7 @@ export interface DesktopRuntimeHostIdentity extends DesktopRuntimeHostRef { readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: RuntimeHostProfileAccess; + readonly collaborationAuthority?: boolean; readonly readiness: 'ready' | 'reconnecting'; } diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index bcb2babbf9..e7bcee80c4 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -129,7 +129,10 @@ import { resolveRuntimeHostSessionCatalog, type RuntimeHostSessionCatalogCoverage, } from './runtime-host-session-catalog.js'; -import { collectAvailablePendingTurnRequests } from './runtime-host-turn-request-inbox.js'; +import { + collectAvailablePendingTurnRequests, + selectRuntimeHostCollaborationScopes, +} from './runtime-host-turn-request-inbox.js'; import type { ExecutionBoundaryReadModel, SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant'; import type { @@ -270,6 +273,7 @@ const runtimeHostMetadata = new Map< readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: 'owner' | 'session_guest'; + readonly collaborationAuthority?: boolean; } >(); const runtimeHostSessionProfiles = new Map(); @@ -337,6 +341,9 @@ ipcRenderer.on( profileName: change.profileName, profileKind: change.profileKind, profileAccess: change.profileAccess, + ...(typeof change.collaborationAuthority === 'boolean' + ? { collaborationAuthority: change.collaborationAuthority } + : {}), }); if (change.isDefault) activeRuntimeHost = nextScope; } else if (change.isDefault) { @@ -364,6 +371,7 @@ function recordRuntimeHostIdentity(value: unknown): { profileName?: unknown; profileKind?: unknown; profileAccess?: unknown; + collaborationAuthority?: unknown; readiness?: unknown; }; if ( @@ -383,6 +391,9 @@ function recordRuntimeHostIdentity(value: unknown): { profileName: metadata.profileName, profileKind: metadata.profileKind, profileAccess: metadata.profileAccess, + ...(typeof metadata.collaborationAuthority === 'boolean' + ? { collaborationAuthority: metadata.collaborationAuthority } + : {}), }); return { scope, readiness: metadata.readiness }; } @@ -1464,9 +1475,14 @@ const makaBridge = { ); }, async getPendingTurnRequests() { - const scopes = (await runtimeHostScopeList()).filter( - (scope) => runtimeHostMetadataFor(scope)?.profileAccess === 'owner', - ); + const scopes = selectRuntimeHostCollaborationScopes( + (await runtimeHostScopeList()).flatMap((scope) => { + const metadata = runtimeHostMetadataFor(scope); + return metadata?.profileAccess === 'owner' + ? [{ scope, collaborationAuthority: metadata.collaborationAuthority }] + : []; + }), + ).map(({ scope }) => scope); return collectAvailablePendingTurnRequests( scopes.map(async (scope) => { const result = await ipcRenderer.invoke( diff --git a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts index 86199b9187..629b769393 100644 --- a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts +++ b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts @@ -19,6 +19,17 @@ import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; +export interface RuntimeHostCollaborationScope { + readonly collaborationAuthority?: boolean; +} + +/** Hosts with an explicit negative capability cannot answer collaboration queries. */ +export function selectRuntimeHostCollaborationScopes( + scopes: readonly T[], +): T[] { + return scopes.filter((scope) => scope.collaborationAuthority !== false); +} + export async function collectAvailablePendingTurnRequests( queries: readonly Promise[], ): Promise { diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts index 9ac1cfde4f..9d025f34d8 100644 --- a/packages/runtime-host/src/__tests__/protocol.test.ts +++ b/packages/runtime-host/src/__tests__/protocol.test.ts @@ -2174,6 +2174,19 @@ describe('Runtime Host bootstrap protocol', () => { }, }; assert.deepEqual(HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput(status), status); + assert.equal( + HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ + ...status, + collaborationAuthority: false, + }).collaborationAuthority, + false, + ); + assert.throws(() => + HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ + ...status, + collaborationAuthority: 'unknown', + }), + ); assert.throws(() => HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ ...status, diff --git a/packages/runtime-host/src/protocol/host-status.ts b/packages/runtime-host/src/protocol/host-status.ts index 5ec5bd51fd..aa6d7eed34 100644 --- a/packages/runtime-host/src/protocol/host-status.ts +++ b/packages/runtime-host/src/protocol/host-status.ts @@ -68,6 +68,8 @@ export interface HostStatusResult { hostEpoch: string; compositionId: string; compositionRevision: string; + /** Whether this Host can serve collaboration authority operations. */ + collaborationAuthority?: boolean; state: HostLifecycleState; connections: number; activeOperations: number; @@ -126,6 +128,7 @@ function decodeHostStatusResult(value: unknown): HostStatusResult { 'hostEpoch', 'compositionId', 'compositionRevision', + ...(valueRecord.collaborationAuthority === undefined ? [] : ['collaborationAuthority']), 'state', 'connections', 'activeOperations', @@ -146,6 +149,7 @@ function decodeHostDiagnosticsResult(value: unknown): HostDiagnosticsResult { 'hostEpoch', 'compositionId', 'compositionRevision', + ...(valueRecord.collaborationAuthority === undefined ? [] : ['collaborationAuthority']), 'state', 'connections', 'activeOperations', @@ -277,6 +281,14 @@ function decodeHostStatusFields(record: Record): HostStatusResu 'Runtime Host composition revision', 128, ), + ...(record.collaborationAuthority === undefined + ? {} + : { + collaborationAuthority: requireBoolean( + record.collaborationAuthority, + 'collaborationAuthority', + ), + }), state: requireHostLifecycleState(record.state), connections: requireCount(record.connections, 'connections'), activeOperations: requireCount(record.activeOperations, 'activeOperations'), diff --git a/packages/runtime-host/src/server/host-kernel.ts b/packages/runtime-host/src/server/host-kernel.ts index cd6df6702b..c7b679e1c9 100644 --- a/packages/runtime-host/src/server/host-kernel.ts +++ b/packages/runtime-host/src/server/host-kernel.ts @@ -845,6 +845,7 @@ export class RuntimeHostKernel { hostEpoch: this.hostEpoch, compositionId: this.compositionDescriptor.id, compositionRevision: this.compositionDescriptor.revision, + collaborationAuthority: this.#options.accessAuthority !== undefined, state: this.#state, connections: this.#acceptedTransports.size, activeOperations: this.#activeOperations, From 171cd6ba5d15071a784110ee16ffb301565dd796 Mon Sep 17 00:00:00 2001 From: testikun Date: Wed, 2 Sep 2026 11:24:55 +0800 Subject: [PATCH 2/6] fix(desktop): preserve scoped collaboration query failures Generated-by: Codex --- ...me-host-turn-request-inbox-preload.test.ts | 3 ++- apps/desktop/src/preload/preload.ts | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts index d1e370e112..1bf016222f 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts @@ -29,9 +29,10 @@ test('skips an Owner Host that explicitly lacks collaboration authority', () => const scopes = selectRuntimeHostCollaborationScopes([ { hostId: 'local', collaborationAuthority: false }, { hostId: 'remote', collaborationAuthority: true }, + { hostId: 'legacy' }, ]); - assert.deepEqual(scopes.map(({ hostId }) => hostId), ['remote']); + assert.deepEqual(scopes.map(({ hostId }) => hostId), ['remote', 'legacy']); }); test('keeps transiently unavailable collaboration inboxes retryable', async () => { diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index e7bcee80c4..db2f133b96 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -292,6 +292,15 @@ function runtimeHostMetadataFor(scope: DesktopTargetScope) { return runtimeHostMetadata.get(runtimeHostScopeKey(scope)); } +function markRuntimeHostCollaborationUnavailable(scope: DesktopTargetScope): void { + const metadata = runtimeHostMetadataFor(scope); + if (!metadata || metadata.collaborationAuthority === false) return; + runtimeHostMetadata.set(runtimeHostScopeKey(scope), { + ...metadata, + collaborationAuthority: false, + }); +} + function observeRuntimeHostSessionScope(scope: DesktopTargetScope, sessionId: string): { readonly sessionId: string; readonly authorityAccepted: boolean; @@ -379,6 +388,8 @@ function recordRuntimeHostIdentity(value: unknown): { typeof metadata.profileName !== 'string' || !isRuntimeHostProfileKind(metadata.profileKind) || (metadata.profileAccess !== 'owner' && metadata.profileAccess !== 'session_guest') || + (metadata.collaborationAuthority !== undefined && + typeof metadata.collaborationAuthority !== 'boolean') || (metadata.readiness !== 'ready' && metadata.readiness !== 'reconnecting') ) { throw new Error('Desktop Runtime Host identity is invalid'); @@ -391,9 +402,9 @@ function recordRuntimeHostIdentity(value: unknown): { profileName: metadata.profileName, profileKind: metadata.profileKind, profileAccess: metadata.profileAccess, - ...(typeof metadata.collaborationAuthority === 'boolean' - ? { collaborationAuthority: metadata.collaborationAuthority } - : {}), + ...(metadata.collaborationAuthority === undefined + ? {} + : { collaborationAuthority: metadata.collaborationAuthority }), }); return { scope, readiness: metadata.readiness }; } @@ -1488,7 +1499,8 @@ const makaBridge = { const result = await ipcRenderer.invoke( 'session-collaboration:turn-request:query', scope, - ) as CollaborationTurnRequestQueryResult; + ) as CollaborationTurnRequestQueryResult & { authorityUnavailable?: true }; + if (result.authorityUnavailable) markRuntimeHostCollaborationUnavailable(scope); return result.requests .filter((request) => request.state.kind === 'pending') .map((request): SessionTurnAccessRequest => ({ From 835cdebbf5b1cbd514511a30953dbe48cb34cd29 Mon Sep 17 00:00:00 2001 From: testikun Date: Wed, 2 Sep 2026 11:27:41 +0800 Subject: [PATCH 3/6] fix(desktop): retain learned collaboration capability Generated-by: Codex --- ...me-host-turn-request-inbox-preload.test.ts | 6 ++++++ apps/desktop/src/preload/preload.ts | 21 +++++++++++++------ .../runtime-host-turn-request-inbox.ts | 7 +++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts index 1bf016222f..e9dd5a8a95 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts @@ -22,9 +22,15 @@ import test from 'node:test'; import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; import { collectAvailablePendingTurnRequests, + retainRuntimeHostCollaborationAuthority, selectRuntimeHostCollaborationScopes, } from '../../preload/runtime-host-turn-request-inbox.js'; +test('retains a learned unavailable capability when a legacy identity omits it', () => { + assert.equal(retainRuntimeHostCollaborationAuthority(undefined, false), false); + assert.equal(retainRuntimeHostCollaborationAuthority(true, false), true); +}); + test('skips an Owner Host that explicitly lacks collaboration authority', () => { const scopes = selectRuntimeHostCollaborationScopes([ { hostId: 'local', collaborationAuthority: false }, diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index db2f133b96..df468d4240 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -131,6 +131,7 @@ import { } from './runtime-host-session-catalog.js'; import { collectAvailablePendingTurnRequests, + retainRuntimeHostCollaborationAuthority, selectRuntimeHostCollaborationScopes, } from './runtime-host-turn-request-inbox.js'; import type { ExecutionBoundaryReadModel, SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; @@ -343,6 +344,13 @@ ipcRenderer.on( } } if (nextScope && nextScopeKey) { + const previousMetadata = runtimeHostMetadata.get(nextScopeKey); + const collaborationAuthority = retainRuntimeHostCollaborationAuthority( + typeof change.collaborationAuthority === 'boolean' + ? change.collaborationAuthority + : undefined, + previousMetadata?.collaborationAuthority, + ); runtimeHostScopes.set(nextScopeKey, nextScope); runtimeHostProfiles.set(change.profileId, nextScopeKey); runtimeHostMetadata.set(nextScopeKey, { @@ -350,9 +358,7 @@ ipcRenderer.on( profileName: change.profileName, profileKind: change.profileKind, profileAccess: change.profileAccess, - ...(typeof change.collaborationAuthority === 'boolean' - ? { collaborationAuthority: change.collaborationAuthority } - : {}), + ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), }); if (change.isDefault) activeRuntimeHost = nextScope; } else if (change.isDefault) { @@ -395,6 +401,11 @@ function recordRuntimeHostIdentity(value: unknown): { throw new Error('Desktop Runtime Host identity is invalid'); } const scopeKey = runtimeHostScopeKey(scope); + const previousMetadata = runtimeHostMetadata.get(scopeKey); + const collaborationAuthority = retainRuntimeHostCollaborationAuthority( + metadata.collaborationAuthority as boolean | undefined, + previousMetadata?.collaborationAuthority, + ); runtimeHostScopes.set(scopeKey, scope); runtimeHostProfiles.set(metadata.profileId, scopeKey); runtimeHostMetadata.set(scopeKey, { @@ -402,9 +413,7 @@ function recordRuntimeHostIdentity(value: unknown): { profileName: metadata.profileName, profileKind: metadata.profileKind, profileAccess: metadata.profileAccess, - ...(metadata.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: metadata.collaborationAuthority }), + ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), }); return { scope, readiness: metadata.readiness }; } diff --git a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts index 629b769393..ae78f33aea 100644 --- a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts +++ b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts @@ -23,6 +23,13 @@ export interface RuntimeHostCollaborationScope { readonly collaborationAuthority?: boolean; } +export function retainRuntimeHostCollaborationAuthority( + observed: boolean | undefined, + previous: boolean | undefined, +): boolean | undefined { + return observed ?? previous; +} + /** Hosts with an explicit negative capability cannot answer collaboration queries. */ export function selectRuntimeHostCollaborationScopes( scopes: readonly T[], From 75c565ef6a4abae248dbec10ab14eaa2dbe9b1ef Mon Sep 17 00:00:00 2001 From: testikun Date: Thu, 3 Sep 2026 10:00:50 +0800 Subject: [PATCH 4/6] test(desktop): cover cached collaboration authority polling Generated-by: Codex --- ...me-host-turn-request-inbox-preload.test.ts | 24 ++++++++++ apps/desktop/src/preload/preload.ts | 45 +++++++++---------- .../runtime-host-turn-request-inbox.ts | 21 +++++++++ 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts index e9dd5a8a95..07fc4790c0 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts @@ -22,6 +22,7 @@ import test from 'node:test'; import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; import { collectAvailablePendingTurnRequests, + collectPendingTurnRequestsWithCapabilityCache, retainRuntimeHostCollaborationAuthority, selectRuntimeHostCollaborationScopes, } from '../../preload/runtime-host-turn-request-inbox.js'; @@ -31,6 +32,29 @@ test('retains a learned unavailable capability when a legacy identity omits it', assert.equal(retainRuntimeHostCollaborationAuthority(true, false), true); }); +test('caches an unavailable legacy Host across polling calls', async () => { + const scope = { hostId: 'legacy' }; + let authority: boolean | undefined; + let queryCalls = 0; + const poll = () => + collectPendingTurnRequestsWithCapabilityCache( + [scope], + () => authority, + async () => { + queryCalls += 1; + return { requests: [], authorityUnavailable: true }; + }, + () => { + authority = false; + }, + ); + + assert.deepEqual(await poll(), []); + assert.equal(authority, false); + assert.deepEqual(await poll(), []); + assert.equal(queryCalls, 1); +}); + test('skips an Owner Host that explicitly lacks collaboration authority', () => { const scopes = selectRuntimeHostCollaborationScopes([ { hostId: 'local', collaborationAuthority: false }, diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index df468d4240..ee9987ced0 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -130,9 +130,8 @@ import { type RuntimeHostSessionCatalogCoverage, } from './runtime-host-session-catalog.js'; import { - collectAvailablePendingTurnRequests, + collectPendingTurnRequestsWithCapabilityCache, retainRuntimeHostCollaborationAuthority, - selectRuntimeHostCollaborationScopes, } from './runtime-host-turn-request-inbox.js'; import type { ExecutionBoundaryReadModel, SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant'; @@ -1495,31 +1494,31 @@ const makaBridge = { ); }, async getPendingTurnRequests() { - const scopes = selectRuntimeHostCollaborationScopes( - (await runtimeHostScopeList()).flatMap((scope) => { - const metadata = runtimeHostMetadataFor(scope); - return metadata?.profileAccess === 'owner' - ? [{ scope, collaborationAuthority: metadata.collaborationAuthority }] - : []; - }), - ).map(({ scope }) => scope); - return collectAvailablePendingTurnRequests( - scopes.map(async (scope) => { + const scopes = (await runtimeHostScopeList()).filter( + (scope) => runtimeHostMetadataFor(scope)?.profileAccess === 'owner', + ); + return collectPendingTurnRequestsWithCapabilityCache( + scopes, + (scope) => runtimeHostMetadataFor(scope)?.collaborationAuthority, + async (scope) => { const result = await ipcRenderer.invoke( 'session-collaboration:turn-request:query', scope, ) as CollaborationTurnRequestQueryResult & { authorityUnavailable?: true }; - if (result.authorityUnavailable) markRuntimeHostCollaborationUnavailable(scope); - return result.requests - .filter((request) => request.state.kind === 'pending') - .map((request): SessionTurnAccessRequest => ({ - ...request, - intent: { - ...request.intent, - sessionId: recordRuntimeHostSessionScope(scope, request.intent.sessionId), - }, - })); - }), + return { + ...(result.authorityUnavailable ? { authorityUnavailable: true as const } : {}), + requests: result.requests + .filter((request) => request.state.kind === 'pending') + .map((request): SessionTurnAccessRequest => ({ + ...request, + intent: { + ...request.intent, + sessionId: recordRuntimeHostSessionScope(scope, request.intent.sessionId), + }, + })), + }; + }, + markRuntimeHostCollaborationUnavailable, ); }, async acknowledgeTurnRequest(sessionId, requestId) { diff --git a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts index ae78f33aea..3ec6cb4b18 100644 --- a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts +++ b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts @@ -23,6 +23,11 @@ export interface RuntimeHostCollaborationScope { readonly collaborationAuthority?: boolean; } +export interface RuntimeHostPendingTurnRequestQuery { + readonly requests: readonly SessionTurnAccessRequest[]; + readonly authorityUnavailable?: true; +} + export function retainRuntimeHostCollaborationAuthority( observed: boolean | undefined, previous: boolean | undefined, @@ -37,6 +42,22 @@ export function selectRuntimeHostCollaborationScopes scope.collaborationAuthority !== false); } +export async function collectPendingTurnRequestsWithCapabilityCache( + scopes: readonly T[], + collaborationAuthority: (scope: T) => boolean | undefined, + query: (scope: T) => Promise, + markAuthorityUnavailable: (scope: T) => void, +): Promise { + const eligibleScopes = scopes.filter((scope) => collaborationAuthority(scope) !== false); + return collectAvailablePendingTurnRequests( + eligibleScopes.map(async (scope) => { + const result = await query(scope); + if (result.authorityUnavailable) markAuthorityUnavailable(scope); + return result.requests; + }), + ); +} + export async function collectAvailablePendingTurnRequests( queries: readonly Promise[], ): Promise { From 7b1bda761b99605e1d241758920fdc04b6b9c090 Mon Sep 17 00:00:00 2001 From: testikun Date: Sat, 5 Sep 2026 10:06:10 +0800 Subject: [PATCH 5/6] fix(desktop): memoize unavailable collaboration inboxes --- ...untime-host-collaboration-ipc-main.test.ts | 7 ++ .../runtime-host-desktop-manager.test.ts | 14 ---- ...me-host-turn-request-inbox-preload.test.ts | 54 +------------ apps/desktop/src/main/runtime-host-boot.ts | 20 +---- apps/desktop/src/main/runtime-host-client.ts | 4 +- .../runtime-host-collaboration-ipc-main.ts | 8 +- .../src/main/runtime-host-desktop-manager.ts | 40 +--------- apps/desktop/src/preload/bridge-contract.d.ts | 2 - apps/desktop/src/preload/preload.ts | 75 +++++++------------ .../runtime-host-turn-request-inbox.ts | 39 ---------- .../src/__tests__/protocol.test.ts | 13 ---- .../runtime-host/src/protocol/host-status.ts | 12 --- .../runtime-host/src/server/host-kernel.ts | 1 - 13 files changed, 43 insertions(+), 246 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts index b5262a3c69..db58a8fb33 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts @@ -165,9 +165,11 @@ function peerReachability() { test('treats an unavailable collaboration authority as an empty background inbox', async () => { const handlers = new Map(); + let queryCalls = 0; registerRuntimeHostCollaborationIpc( { async queryCollaborationTurnRequests() { + queryCalls += 1; throw new RuntimeHostOperationError( 'collaboration.turn-request.query', 'operation_unavailable', @@ -192,6 +194,11 @@ test('treats an unavailable collaboration authority as an empty background inbox requests: [], authorityUnavailable: true, }); + assert.deepEqual(await query({} as Parameters[0]), { + canRequestTurns: false, + requests: [], + }); + assert.equal(queryCalls, 1); await assert.rejects( query({} as Parameters[0], 'session-1'), RuntimeHostOperationError, diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts index 829d4ef36e..b2392b6b5b 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts @@ -110,20 +110,6 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy assert.equal(second.closeCalls, 1); }); -test('publishes the Runtime Host collaboration capability with its identity', async () => { - const current = candidateHarness(); - (current.candidate.client as unknown as { - status: () => Promise<{ collaborationAuthority: boolean }>; - }).status = async () => ({ collaborationAuthority: false }); - const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, { - startCandidate: async () => ready(current.candidate), - }); - - assert.equal(owner.current()?.collaborationAuthority, false); - assert.equal(owner.entries()[0]?.collaborationAuthority, false); - await owner.close(); -}); - test('quiesces reconnect and waits for the Host process before update install', async () => { const current = candidateHarness({ disconnectOnPrepare: true }); const replacement = candidateHarness(); diff --git a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts index 07fc4790c0..267504c866 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-turn-request-inbox-preload.test.ts @@ -20,59 +20,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; -import { - collectAvailablePendingTurnRequests, - collectPendingTurnRequestsWithCapabilityCache, - retainRuntimeHostCollaborationAuthority, - selectRuntimeHostCollaborationScopes, -} from '../../preload/runtime-host-turn-request-inbox.js'; - -test('retains a learned unavailable capability when a legacy identity omits it', () => { - assert.equal(retainRuntimeHostCollaborationAuthority(undefined, false), false); - assert.equal(retainRuntimeHostCollaborationAuthority(true, false), true); -}); - -test('caches an unavailable legacy Host across polling calls', async () => { - const scope = { hostId: 'legacy' }; - let authority: boolean | undefined; - let queryCalls = 0; - const poll = () => - collectPendingTurnRequestsWithCapabilityCache( - [scope], - () => authority, - async () => { - queryCalls += 1; - return { requests: [], authorityUnavailable: true }; - }, - () => { - authority = false; - }, - ); - - assert.deepEqual(await poll(), []); - assert.equal(authority, false); - assert.deepEqual(await poll(), []); - assert.equal(queryCalls, 1); -}); - -test('skips an Owner Host that explicitly lacks collaboration authority', () => { - const scopes = selectRuntimeHostCollaborationScopes([ - { hostId: 'local', collaborationAuthority: false }, - { hostId: 'remote', collaborationAuthority: true }, - { hostId: 'legacy' }, - ]); - - assert.deepEqual(scopes.map(({ hostId }) => hostId), ['remote', 'legacy']); -}); - -test('keeps transiently unavailable collaboration inboxes retryable', async () => { - const requests = await collectAvailablePendingTurnRequests([ - Promise.reject(new Error('connection lost while polling')), - Promise.resolve([request('available', '2026-09-01T00:00:01.000Z')]), - ]); - - assert.deepEqual(requests.map(({ requestId }) => requestId), ['available']); -}); +import { collectAvailablePendingTurnRequests } from '../../preload/runtime-host-turn-request-inbox.js'; function request(requestId: string, createdAt: string): SessionTurnAccessRequest { return { requestId, createdAt } as SessionTurnAccessRequest; diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 5c21ae3b73..7caee1cf13 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1135,9 +1135,6 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state.target.profile.name, profileKind: state.target.profile.kind, profileAccess, - ...(state.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: state.collaborationAuthority }), ...(hostId ? { hostId } : {}), readiness: state.readiness, isDefault: @@ -1188,9 +1185,6 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state.target.profile.name, profileKind: state.target.profile.kind, profileAccess: runtimeHostProfileAccess(state.target.profile), - ...(state.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: state.collaborationAuthority }), ...(hostId ? { hostId } : {}), readiness: "unavailable", isDefault: @@ -1215,9 +1209,6 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( profileName: state?.target.profile.name ?? profileId, profileKind: state?.target.profile.kind ?? "remote", profileAccess: state ? runtimeHostProfileAccess(state.target.profile) : "owner", - ...(state?.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: state.collaborationAuthority }), ...(state?.readiness === "ready" ? { hostId: state.candidate.client.hostId } : state?.readiness !== "unavailable" && state && "hostId" in state && state.hostId @@ -1818,7 +1809,6 @@ function registerPersistentClientIpc(): void { target: ResolvedRuntimeHostProfile, readiness: 'ready' | 'reconnecting', hostId: string, - collaborationAuthority?: boolean, ): DesktopRuntimeHostIdentity => ({ hostId, targetEpoch: epoch, @@ -1826,7 +1816,6 @@ function registerPersistentClientIpc(): void { profileName: target.profile.name, profileKind: target.profile.kind, profileAccess: runtimeHostProfileAccess(target.profile), - ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), readiness, }); ipcMain.handle("runtime-host:activeIdentity", () => { @@ -1839,7 +1828,6 @@ function registerPersistentClientIpc(): void { current.target, current.readiness, current.hostId, - current.collaborationAuthority, ); }); ipcMain.handle("runtime-host:identities", () => @@ -1848,13 +1836,7 @@ function registerPersistentClientIpc(): void { const hostId = state.readiness === "ready" ? state.candidate.client.hostId : state.hostId; if (!hostId) return []; return [ - projectRuntimeHostIdentity( - state.epoch, - state.target, - state.readiness, - hostId, - state.collaborationAuthority, - ), + projectRuntimeHostIdentity(state.epoch, state.target, state.readiness, hostId), ]; }), ); diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index 4ae1411d6e..f548396cb5 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -302,8 +302,8 @@ export class DesktopRuntimeHostClient { return this.#connectionClosed || this.#closeTask ? 'unavailable' : 'ready'; } - status(timeoutMs?: number): Promise { - return this.connection.status(timeoutMs); + status(): Promise { + return this.connection.status(); } finalizeAccessCredential( diff --git a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts index 8898165129..a2a011a437 100644 --- a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts @@ -19,7 +19,6 @@ import { RuntimeHostOperationError } from '@maka/runtime-host/client'; import type { DesktopRuntimeHostClient } from './runtime-host-client.js'; -import type { CollaborationTurnRequestQueryResult } from '@maka/runtime-host/protocol'; import { encodeDesktopCollaborationInvitation, type DesktopCollaborationConnectionTarget, @@ -47,6 +46,7 @@ export function registerRuntimeHostCollaborationIpc( | DesktopCollaborationConnectionTarget | Promise, ): void { + let backgroundInboxUnavailable = false; ipcMain.handle( 'session-collaboration:prepare', async ( @@ -98,15 +98,19 @@ export function registerRuntimeHostCollaborationIpc( async (_event, sessionId: unknown) => { const requestedSessionId = sessionId === undefined ? undefined : requiredId(sessionId, 'Session'); + if (requestedSessionId === undefined && backgroundInboxUnavailable) { + return { canRequestTurns: false, requests: [] }; + } try { return await client.queryCollaborationTurnRequests(requestedSessionId); } catch (error) { if (requestedSessionId === undefined && isCollaborationInboxUnavailable(error)) { + backgroundInboxUnavailable = true; return { canRequestTurns: false, requests: [], authorityUnavailable: true, - } satisfies CollaborationTurnRequestQueryResult & { authorityUnavailable: true }; + }; } throw error; } diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts index 9f7dc5be20..36d8eca62d 100644 --- a/apps/desktop/src/main/runtime-host-desktop-manager.ts +++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts @@ -108,7 +108,6 @@ export interface RuntimeHostDesktopTargetSnapshot { readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'ready' | 'reconnecting'; readonly candidate?: DesktopRuntimeHostCandidate; - readonly collaborationAuthority?: boolean; } export type RuntimeHostDesktopTargetState = @@ -117,21 +116,18 @@ export type RuntimeHostDesktopTargetState = readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'connecting' | 'reconnecting'; readonly hostId?: string; - readonly collaborationAuthority?: boolean; } | { readonly epoch: string; readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'ready'; readonly candidate: DesktopRuntimeHostCandidate; - readonly collaborationAuthority?: boolean; } | { readonly epoch: string; readonly target: ResolvedRuntimeHostProfile; readonly readiness: 'unavailable'; readonly hostId?: string; - readonly collaborationAuthority?: boolean; readonly error: Error; }; @@ -223,7 +219,6 @@ interface DesktopRuntimeHostTargetGeneration { readonly observations: RuntimeHostSessionObservationRegistry; state: RuntimeHostDesktopTargetState; hostId?: string; - collaborationAuthority?: boolean; lifecycle?: RuntimeHostReconnectLifecycle; unsubscribeLifecycle?: () => void; unsubscribeRoutes?: () => void; @@ -484,20 +479,12 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { ...(target.hostId ? { hostId: target.hostId } : {}), target: target.target, readiness: candidate ? 'ready' : 'reconnecting', - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), ...(candidate ? { candidate } : {}), }; } entries(): readonly RuntimeHostDesktopTargetState[] { - return [...this.#targets.values()].map((target) => ({ - ...target.state, - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), - })); + return [...this.#targets.values()].map((target) => target.state); } ownsScope(scope: { readonly hostId: string; readonly targetEpoch: string }): boolean { @@ -1098,12 +1085,6 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { // replay one-shot join progress. onConnectionPhase: (phase) => onConnectionPhase?.(phase), ...(refreshPeerRoutes ? {} : { refreshPeerRoutes: false }), - onHostStatus: (status) => { - if (status.collaborationAuthority !== undefined) { - target.collaborationAuthority = status.collaborationAuthority; - } - target.input.onHostStatus?.(status); - }, signal, ...(takeoverHostEpoch === undefined ? {} : { takeoverHostEpoch }), }, @@ -1116,13 +1097,6 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { throw error; } if (result.kind === 'ready') { - const status = result.candidate.client.status; - if (typeof status === 'function') { - const observed = await status.call(result.candidate.client, 5_000).catch(() => undefined); - if (observed?.collaborationAuthority !== undefined) { - target.collaborationAuthority = observed.collaborationAuthority; - } - } target.hostId = result.candidate.client.hostId; const previous = target.lastCandidate; const retainedOwnedProcess = @@ -1379,18 +1353,12 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { target: target.target, readiness: 'ready', candidate, - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), } : { epoch: target.epoch, target: target.target, readiness: 'reconnecting', ...(target.hostId ? { hostId: target.hostId } : {}), - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), }, ); }); @@ -1403,18 +1371,12 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { target: target.target, readiness: 'ready', candidate, - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), } : { epoch: target.epoch, target: target.target, readiness: 'reconnecting', ...(target.hostId ? { hostId: target.hostId } : {}), - ...(target.collaborationAuthority === undefined - ? {} - : { collaborationAuthority: target.collaborationAuthority }), }, ); } diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index 3e6b6d0918..70b940e6b6 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -452,7 +452,6 @@ export interface DesktopRuntimeHostProfileChangedEvent { readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: RuntimeHostProfileAccess; - readonly collaborationAuthority?: boolean; readonly readiness: 'connecting' | 'ready' | 'reconnecting' | 'unavailable'; readonly hostId?: string; readonly isDefault: boolean; @@ -464,7 +463,6 @@ export interface DesktopRuntimeHostIdentity extends DesktopRuntimeHostRef { readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: RuntimeHostProfileAccess; - readonly collaborationAuthority?: boolean; readonly readiness: 'ready' | 'reconnecting'; } diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index ee9987ced0..56807cf86f 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -129,10 +129,7 @@ import { resolveRuntimeHostSessionCatalog, type RuntimeHostSessionCatalogCoverage, } from './runtime-host-session-catalog.js'; -import { - collectPendingTurnRequestsWithCapabilityCache, - retainRuntimeHostCollaborationAuthority, -} from './runtime-host-turn-request-inbox.js'; +import { collectAvailablePendingTurnRequests } from './runtime-host-turn-request-inbox.js'; import type { ExecutionBoundaryReadModel, SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant'; import type { @@ -273,10 +270,10 @@ const runtimeHostMetadata = new Map< readonly profileName: string; readonly profileKind: RuntimeHostProfileKind; readonly profileAccess: 'owner' | 'session_guest'; - readonly collaborationAuthority?: boolean; } >(); const runtimeHostSessionProfiles = new Map(); +const unavailableCollaborationScopes = new Set(); let lastDesktopSessionCatalog: RuntimeHostSessionCatalogCoverage = { sessions: [], completeHostIds: [], @@ -292,15 +289,6 @@ function runtimeHostMetadataFor(scope: DesktopTargetScope) { return runtimeHostMetadata.get(runtimeHostScopeKey(scope)); } -function markRuntimeHostCollaborationUnavailable(scope: DesktopTargetScope): void { - const metadata = runtimeHostMetadataFor(scope); - if (!metadata || metadata.collaborationAuthority === false) return; - runtimeHostMetadata.set(runtimeHostScopeKey(scope), { - ...metadata, - collaborationAuthority: false, - }); -} - function observeRuntimeHostSessionScope(scope: DesktopTargetScope, sessionId: string): { readonly sessionId: string; readonly authorityAccepted: boolean; @@ -343,13 +331,6 @@ ipcRenderer.on( } } if (nextScope && nextScopeKey) { - const previousMetadata = runtimeHostMetadata.get(nextScopeKey); - const collaborationAuthority = retainRuntimeHostCollaborationAuthority( - typeof change.collaborationAuthority === 'boolean' - ? change.collaborationAuthority - : undefined, - previousMetadata?.collaborationAuthority, - ); runtimeHostScopes.set(nextScopeKey, nextScope); runtimeHostProfiles.set(change.profileId, nextScopeKey); runtimeHostMetadata.set(nextScopeKey, { @@ -357,7 +338,6 @@ ipcRenderer.on( profileName: change.profileName, profileKind: change.profileKind, profileAccess: change.profileAccess, - ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), }); if (change.isDefault) activeRuntimeHost = nextScope; } else if (change.isDefault) { @@ -385,7 +365,6 @@ function recordRuntimeHostIdentity(value: unknown): { profileName?: unknown; profileKind?: unknown; profileAccess?: unknown; - collaborationAuthority?: unknown; readiness?: unknown; }; if ( @@ -393,18 +372,11 @@ function recordRuntimeHostIdentity(value: unknown): { typeof metadata.profileName !== 'string' || !isRuntimeHostProfileKind(metadata.profileKind) || (metadata.profileAccess !== 'owner' && metadata.profileAccess !== 'session_guest') || - (metadata.collaborationAuthority !== undefined && - typeof metadata.collaborationAuthority !== 'boolean') || (metadata.readiness !== 'ready' && metadata.readiness !== 'reconnecting') ) { throw new Error('Desktop Runtime Host identity is invalid'); } const scopeKey = runtimeHostScopeKey(scope); - const previousMetadata = runtimeHostMetadata.get(scopeKey); - const collaborationAuthority = retainRuntimeHostCollaborationAuthority( - metadata.collaborationAuthority as boolean | undefined, - previousMetadata?.collaborationAuthority, - ); runtimeHostScopes.set(scopeKey, scope); runtimeHostProfiles.set(metadata.profileId, scopeKey); runtimeHostMetadata.set(scopeKey, { @@ -412,7 +384,6 @@ function recordRuntimeHostIdentity(value: unknown): { profileName: metadata.profileName, profileKind: metadata.profileKind, profileAccess: metadata.profileAccess, - ...(collaborationAuthority === undefined ? {} : { collaborationAuthority }), }); return { scope, readiness: metadata.readiness }; } @@ -1494,31 +1465,35 @@ const makaBridge = { ); }, async getPendingTurnRequests() { - const scopes = (await runtimeHostScopeList()).filter( + const readyScopes = (await runtimeHostScopeList()).filter( (scope) => runtimeHostMetadataFor(scope)?.profileAccess === 'owner', ); - return collectPendingTurnRequestsWithCapabilityCache( - scopes, - (scope) => runtimeHostMetadataFor(scope)?.collaborationAuthority, - async (scope) => { + const readyScopeKeys = new Set(readyScopes.map(runtimeHostScopeKey)); + for (const scopeKey of unavailableCollaborationScopes) { + if (!readyScopeKeys.has(scopeKey)) unavailableCollaborationScopes.delete(scopeKey); + } + const scopes = readyScopes.filter( + (scope) => !unavailableCollaborationScopes.has(runtimeHostScopeKey(scope)), + ); + return collectAvailablePendingTurnRequests( + scopes.map(async (scope) => { const result = await ipcRenderer.invoke( 'session-collaboration:turn-request:query', scope, ) as CollaborationTurnRequestQueryResult & { authorityUnavailable?: true }; - return { - ...(result.authorityUnavailable ? { authorityUnavailable: true as const } : {}), - requests: result.requests - .filter((request) => request.state.kind === 'pending') - .map((request): SessionTurnAccessRequest => ({ - ...request, - intent: { - ...request.intent, - sessionId: recordRuntimeHostSessionScope(scope, request.intent.sessionId), - }, - })), - }; - }, - markRuntimeHostCollaborationUnavailable, + if (result.authorityUnavailable) { + unavailableCollaborationScopes.add(runtimeHostScopeKey(scope)); + } + return result.requests + .filter((request) => request.state.kind === 'pending') + .map((request): SessionTurnAccessRequest => ({ + ...request, + intent: { + ...request.intent, + sessionId: recordRuntimeHostSessionScope(scope, request.intent.sessionId), + }, + })); + }), ); }, async acknowledgeTurnRequest(sessionId, requestId) { diff --git a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts index 3ec6cb4b18..86199b9187 100644 --- a/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts +++ b/apps/desktop/src/preload/runtime-host-turn-request-inbox.ts @@ -19,45 +19,6 @@ import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol'; -export interface RuntimeHostCollaborationScope { - readonly collaborationAuthority?: boolean; -} - -export interface RuntimeHostPendingTurnRequestQuery { - readonly requests: readonly SessionTurnAccessRequest[]; - readonly authorityUnavailable?: true; -} - -export function retainRuntimeHostCollaborationAuthority( - observed: boolean | undefined, - previous: boolean | undefined, -): boolean | undefined { - return observed ?? previous; -} - -/** Hosts with an explicit negative capability cannot answer collaboration queries. */ -export function selectRuntimeHostCollaborationScopes( - scopes: readonly T[], -): T[] { - return scopes.filter((scope) => scope.collaborationAuthority !== false); -} - -export async function collectPendingTurnRequestsWithCapabilityCache( - scopes: readonly T[], - collaborationAuthority: (scope: T) => boolean | undefined, - query: (scope: T) => Promise, - markAuthorityUnavailable: (scope: T) => void, -): Promise { - const eligibleScopes = scopes.filter((scope) => collaborationAuthority(scope) !== false); - return collectAvailablePendingTurnRequests( - eligibleScopes.map(async (scope) => { - const result = await query(scope); - if (result.authorityUnavailable) markAuthorityUnavailable(scope); - return result.requests; - }), - ); -} - export async function collectAvailablePendingTurnRequests( queries: readonly Promise[], ): Promise { diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts index 9d025f34d8..9ac1cfde4f 100644 --- a/packages/runtime-host/src/__tests__/protocol.test.ts +++ b/packages/runtime-host/src/__tests__/protocol.test.ts @@ -2174,19 +2174,6 @@ describe('Runtime Host bootstrap protocol', () => { }, }; assert.deepEqual(HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput(status), status); - assert.equal( - HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ - ...status, - collaborationAuthority: false, - }).collaborationAuthority, - false, - ); - assert.throws(() => - HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ - ...status, - collaborationAuthority: 'unknown', - }), - ); assert.throws(() => HOST_BOOTSTRAP_OPERATION_SPECS['host.status'].decodeOutput({ ...status, diff --git a/packages/runtime-host/src/protocol/host-status.ts b/packages/runtime-host/src/protocol/host-status.ts index aa6d7eed34..5ec5bd51fd 100644 --- a/packages/runtime-host/src/protocol/host-status.ts +++ b/packages/runtime-host/src/protocol/host-status.ts @@ -68,8 +68,6 @@ export interface HostStatusResult { hostEpoch: string; compositionId: string; compositionRevision: string; - /** Whether this Host can serve collaboration authority operations. */ - collaborationAuthority?: boolean; state: HostLifecycleState; connections: number; activeOperations: number; @@ -128,7 +126,6 @@ function decodeHostStatusResult(value: unknown): HostStatusResult { 'hostEpoch', 'compositionId', 'compositionRevision', - ...(valueRecord.collaborationAuthority === undefined ? [] : ['collaborationAuthority']), 'state', 'connections', 'activeOperations', @@ -149,7 +146,6 @@ function decodeHostDiagnosticsResult(value: unknown): HostDiagnosticsResult { 'hostEpoch', 'compositionId', 'compositionRevision', - ...(valueRecord.collaborationAuthority === undefined ? [] : ['collaborationAuthority']), 'state', 'connections', 'activeOperations', @@ -281,14 +277,6 @@ function decodeHostStatusFields(record: Record): HostStatusResu 'Runtime Host composition revision', 128, ), - ...(record.collaborationAuthority === undefined - ? {} - : { - collaborationAuthority: requireBoolean( - record.collaborationAuthority, - 'collaborationAuthority', - ), - }), state: requireHostLifecycleState(record.state), connections: requireCount(record.connections, 'connections'), activeOperations: requireCount(record.activeOperations, 'activeOperations'), diff --git a/packages/runtime-host/src/server/host-kernel.ts b/packages/runtime-host/src/server/host-kernel.ts index c7b679e1c9..cd6df6702b 100644 --- a/packages/runtime-host/src/server/host-kernel.ts +++ b/packages/runtime-host/src/server/host-kernel.ts @@ -845,7 +845,6 @@ export class RuntimeHostKernel { hostEpoch: this.hostEpoch, compositionId: this.compositionDescriptor.id, compositionRevision: this.compositionDescriptor.revision, - collaborationAuthority: this.#options.accessAuthority !== undefined, state: this.#state, connections: this.#acceptedTransports.size, activeOperations: this.#activeOperations, From b4e530fdbe80fc1e502b19c96f48f7ebdcb7d0b0 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 5 Sep 2026 11:43:50 +0800 Subject: [PATCH 6/6] fix(desktop): keep collaboration unavailability memo in main --- .../runtime-host-collaboration-ipc-main.test.ts | 1 - .../main/runtime-host-collaboration-ipc-main.ts | 6 +----- apps/desktop/src/preload/preload.ts | 15 ++------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts index db58a8fb33..a161321e20 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-collaboration-ipc-main.test.ts @@ -192,7 +192,6 @@ test('treats an unavailable collaboration authority as an empty background inbox assert.deepEqual(await query({} as Parameters[0]), { canRequestTurns: false, requests: [], - authorityUnavailable: true, }); assert.deepEqual(await query({} as Parameters[0]), { canRequestTurns: false, diff --git a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts index a2a011a437..47e5a677fc 100644 --- a/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts @@ -106,11 +106,7 @@ export function registerRuntimeHostCollaborationIpc( } catch (error) { if (requestedSessionId === undefined && isCollaborationInboxUnavailable(error)) { backgroundInboxUnavailable = true; - return { - canRequestTurns: false, - requests: [], - authorityUnavailable: true, - }; + return { canRequestTurns: false, requests: [] }; } throw error; } diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 56807cf86f..bcb2babbf9 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -273,7 +273,6 @@ const runtimeHostMetadata = new Map< } >(); const runtimeHostSessionProfiles = new Map(); -const unavailableCollaborationScopes = new Set(); let lastDesktopSessionCatalog: RuntimeHostSessionCatalogCoverage = { sessions: [], completeHostIds: [], @@ -1465,25 +1464,15 @@ const makaBridge = { ); }, async getPendingTurnRequests() { - const readyScopes = (await runtimeHostScopeList()).filter( + const scopes = (await runtimeHostScopeList()).filter( (scope) => runtimeHostMetadataFor(scope)?.profileAccess === 'owner', ); - const readyScopeKeys = new Set(readyScopes.map(runtimeHostScopeKey)); - for (const scopeKey of unavailableCollaborationScopes) { - if (!readyScopeKeys.has(scopeKey)) unavailableCollaborationScopes.delete(scopeKey); - } - const scopes = readyScopes.filter( - (scope) => !unavailableCollaborationScopes.has(runtimeHostScopeKey(scope)), - ); return collectAvailablePendingTurnRequests( scopes.map(async (scope) => { const result = await ipcRenderer.invoke( 'session-collaboration:turn-request:query', scope, - ) as CollaborationTurnRequestQueryResult & { authorityUnavailable?: true }; - if (result.authorityUnavailable) { - unavailableCollaborationScopes.add(runtimeHostScopeKey(scope)); - } + ) as CollaborationTurnRequestQueryResult; return result.requests .filter((request) => request.state.kind === 'pending') .map((request): SessionTurnAccessRequest => ({