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 () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: this does not cross the boundary @Phoenix500526 asked about. The cache here is a local authority variable and a local mark closure, not runtimeHostMetadata and markRuntimeHostCollaborationUnavailable. Replace the callback passed at preload.ts:1418 with a no-op, or have it write the wrong scope key, and this test still passes, which is exactly the failure mode the request was meant to rule out.

Assert against the real pair instead: mark through markRuntimeHostCollaborationUnavailable, then read back through runtimeHostMetadataFor on the second poll.

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 @@ -1140,6 +1140,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:
Expand Down Expand Up @@ -1190,6 +1193,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 @@ -1214,6 +1220,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 @@ -1805,13 +1814,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 @@ -1824,6 +1835,7 @@ function registerPersistentClientIpc(): void {
current.target,
current.readiness,
current.hostId,
current.collaborationAuthority,
);
});
ipcMain.handle("runtime-host:identities", () =>
Expand All @@ -1832,7 +1844,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 @@ -302,8 +302,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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: authorityUnavailable is a cross-process contract field, but it exists only as an ad-hoc intersection at each end, here via satisfies CollaborationTurnRequestQueryResult & { authorityUnavailable: true } and at preload.ts:1404 via a cast. It is not declared in bridge-contract.d.ts, so renaming or dropping it on one side compiles clean on the other. Declare the query result shape once in the bridge contract and have both ends use it.

} 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 @@ -107,6 +107,7 @@ export interface RuntimeHostDesktopTargetSnapshot {
readonly target: ResolvedRuntimeHostProfile;
readonly readiness: 'ready' | 'reconnecting';
readonly candidate?: DesktopRuntimeHostCandidate;
readonly collaborationAuthority?: boolean;
}

export type RuntimeHostDesktopTargetState =
Expand All @@ -115,18 +116,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 @@ -218,6 +222,7 @@ interface DesktopRuntimeHostTargetGeneration {
readonly observations: RuntimeHostSessionObservationRegistry;
state: RuntimeHostDesktopTargetState;
hostId?: string;
collaborationAuthority?: boolean;
lifecycle?: RuntimeHostReconnectLifecycle<DesktopRuntimeHostCandidate>;
unsubscribeLifecycle?: () => void;
unsubscribeRoutes?: () => void;
Expand Down Expand Up @@ -475,12 +480,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) => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: this spreads the generation's collaborationAuthority over target.state, but the state objects already carry the field from the constructors at 1227 to 1266 and from #projectState. One of the two writers is dead. Pick the state as the single carrier and drop the override here, or drop it from the state variants and keep the projection in one place.

...target.state,
...(target.collaborationAuthority === undefined
? {}
: { collaborationAuthority: target.collaborationAuthority }),
}));
}

ownsScope(scope: { readonly hostId: string; readonly targetEpoch: string }): boolean {
Expand Down Expand Up @@ -1081,6 +1094,12 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
// replay one-shot join progress.
onConnectionPhase: (phase) => onConnectionPhase?.(phase),
...(refreshPeerRoutes ? {} : { refreshPeerRoutes: false }),
onHostStatus: (status) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: this overrides onHostStatus unconditionally, after ...target.input. Before, it was only present when a caller supplied one (...(onHostStatus ? { onHostStatus } : {})).

That has a side effect the PR does not mention: connection.ts:833-839 returns early from #resetLivenessCheck when a status observer exists, so ongoing Session traffic no longer defers the liveness timer and host.status keeps firing on schedule at DEFAULT_LIVENESS_INTERVAL_MS = 2_000 (connection.ts:87). An idle connection already probed at that rate, so the increase is bounded, but the direction is the opposite of this PR's premise: to remove one 2 second query, every connection including the ones that do support collaboration gets a 2 second probe that traffic can no longer defer.

Resolve this together with the ready-path probe below. Either keep a main-process memo and register no observer at all, or keep the observer, register it only while the capability is unknown, and drop the explicit probe.

if (status.collaborationAuthority !== undefined) {
target.collaborationAuthority = status.collaborationAuthority;
}
target.input.onHostStatus?.(status);
},
signal,
...(takeoverHostEpoch === undefined ? {} : { takeoverHostEpoch }),
},
Expand All @@ -1093,6 +1112,13 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
throw error;
}
if (result.kind === 'ready') {
const status = result.candidate.client.status;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: this awaits an extra host.status round trip, up to 5 seconds, before target.hostId is set and the ready state is published. Every connect and every reconnect pays it, on a path that has nothing to do with collaboration, and a Host that is connected but slow stalls readiness for the full timeout.

It also looks redundant. The connect path already performs a host.status (client/wait-for-ready.ts:34), and connection.ts:566 hands that result to the onHostStatus observer, which this PR registers a few lines above at 987. So the same fact already arrives on its own, and this is a second acquisition path for it. Smallest fix: delete lines 1005 to 1011 and keep the observer.

P3 on line 1006: typeof status === 'function' guards a method that always exists on DesktopRuntimeHostClient. It is there to tolerate the fake candidate in runtime-host-desktop-manager.test.ts. Fix the harness rather than leaving a test-shaped guard in production code.

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 @@ -1311,12 +1337,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 @@ -1329,12 +1361,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 @@ -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;
Expand All @@ -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';
}

Expand Down
Loading