Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ test('treats an unavailable collaboration authority as an empty background inbox
assert.deepEqual(await query({} as Parameters<IpcHandler>[0]), {
canRequestTurns: false,
requests: [],
authorityUnavailable: true,
});
await assert.rejects(
query({} as Parameters<IpcHandler>[0], 'session-1'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,59 @@
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,
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']);
Comment thread
testikun marked this conversation as resolved.
});

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;
Expand Down
20 changes: 19 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,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: state.readiness,
isDefault:
Expand Down Expand Up @@ -1135,6 +1138,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:
Expand All @@ -1159,6 +1165,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
Expand Down Expand Up @@ -1750,13 +1759,15 @@ function registerPersistentClientIpc(): void {
target: ResolvedRuntimeHostProfile,
readiness: 'ready' | 'reconnecting',
hostId: string,
collaborationAuthority?: boolean,
): DesktopRuntimeHostIdentity => ({
hostId,
targetEpoch: epoch,
profileId: target.profile.id,
profileName: target.profile.name,
profileKind: target.profile.kind,
profileAccess: runtimeHostProfileAccess(target.profile),
...(collaborationAuthority === undefined ? {} : { collaborationAuthority }),
readiness,
});
ipcMain.handle("runtime-host:activeIdentity", () => {
Expand All @@ -1769,6 +1780,7 @@ function registerPersistentClientIpc(): void {
current.target,
current.readiness,
current.hostId,
current.collaborationAuthority,
);
});
ipcMain.handle("runtime-host:identities", () =>
Expand All @@ -1777,7 +1789,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,
),
];
}),
);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ export class DesktopRuntimeHostClient {
return this.#connectionClosed || this.#closeTask ? 'unavailable' : 'ready';
}

status(): Promise<HostStatusResult> {
return this.connection.status();
status(timeoutMs?: number): Promise<HostStatusResult> {
return this.connection.status(timeoutMs);
}

finalizeAccessCredential(
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
40 changes: 39 additions & 1 deletion apps/desktop/src/main/runtime-host-desktop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface RuntimeHostDesktopTargetSnapshot {
readonly target: ResolvedRuntimeHostProfile;
readonly readiness: 'ready' | 'reconnecting';
readonly candidate?: DesktopRuntimeHostCandidate;
readonly collaborationAuthority?: boolean;
}

export type RuntimeHostDesktopTargetState =
Expand All @@ -112,18 +113,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;
};

Expand Down Expand Up @@ -213,6 +217,7 @@ interface DesktopRuntimeHostTargetGeneration {
readonly observations: RuntimeHostSessionObservationRegistry;
state: RuntimeHostDesktopTargetState;
hostId?: string;
collaborationAuthority?: boolean;
lifecycle?: RuntimeHostReconnectLifecycle<DesktopRuntimeHostCandidate>;
unsubscribeLifecycle?: () => void;
unsubscribeRoutes?: () => void;
Expand Down Expand Up @@ -467,12 +472,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 {
Expand Down Expand Up @@ -971,6 +984,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 }),
},
Expand All @@ -983,6 +1002,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 =
Expand Down Expand Up @@ -1201,12 +1227,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 }),
},
);
});
Expand All @@ -1219,12 +1251,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 }),
},
);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,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;
Expand All @@ -462,6 +463,7 @@ export interface DesktopRuntimeHostIdentity extends DesktopRuntimeHostRef {
readonly profileName: string;
readonly profileKind: RuntimeHostProfileKind;
readonly profileAccess: RuntimeHostProfileAccess;
readonly collaborationAuthority?: boolean;
readonly readiness: 'ready' | 'reconnecting';
}

Expand Down
Loading