Skip to content

Commit 5c55dc0

Browse files
committed
refactor(platforms): declare helper and runner state as slots of the one store (#3794)
The Android helper and the Apple runner no longer own a store. Their modules declare slot keys on host-kit's single store, and the daemon's entered handle scopes them. The Apple runner keeps reaching the store through its host port, so no runner module imports host-kit (R77). The host binding createAppleRunnerRuntimeState used to wrap is the existing setAppleRunnerHostEnvironment called inside the runtime, so no per-platform handle factory remains.
1 parent c9dfb45 commit 5c55dc0

14 files changed

Lines changed: 84 additions & 89 deletions

‎packages/platform-android/src/__tests__/snapshot-helper-session-runtime-isolation.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert/strict';
22
import { test } from 'vitest';
3-
import { createAndroidHelperRuntimeState } from '../adb-host.ts';
3+
import { createRuntimeStateHandle } from '@agent-device/host-kit/runtime-slots';
44
import { recoverAndroidSnapshotHelperRetirement } from '../snapshot-helper-retirement.ts';
55
import {
66
resetAndroidSnapshotHelperSessions,
@@ -19,7 +19,7 @@ function runtimeOnDevice(deviceKey: string, options: Partial<SessionProviderOpti
1919
const calls: (readonly string[])[] = [];
2020
const spawnArgs: (readonly string[])[] = [];
2121
const provider = createSessionProvider({ calls, spawnArgs, ...options });
22-
const state = createAndroidHelperRuntimeState();
22+
const state = createRuntimeStateHandle();
2323
return {
2424
calls,
2525
spawnArgs,

‎packages/platform-android/src/adb-host.ts‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { AsyncLocalStorage } from 'node:async_hooks';
2-
import { defineRuntimeSlots } from '@agent-device/host-kit/runtime-slots';
32
import type { AndroidHelperInstallDecision, AndroidImeHelperArtifact } from './helper-artifacts.ts';
43
import type {
54
AndroidAdbInvocation,
@@ -116,19 +115,6 @@ export function bindAndroidAdbHost(host: AndroidAdbHost): void {
116115
boundHost = host;
117116
}
118117

119-
/**
120-
* What one daemon runtime owns of the Android helper: warm sessions, failed starts, pending
121-
* retirements and shell-protocol probes, which the helper modules keep in {@link helperRuntimeSlot}s.
122-
* A close tears down only the runtime that closes; the helper itself is stopped by the close-time
123-
* reset, which reads the same slots.
124-
*/
125-
const helperRuntime = defineRuntimeSlots();
126-
127-
export const createAndroidHelperRuntimeState = helperRuntime.createHandle;
128-
129-
/** One piece of helper state this daemon runtime owns, created on first use. */
130-
export const helperRuntimeSlot = helperRuntime.slot;
131-
132118
export function requireAndroidAdbHost(): AndroidAdbHost {
133119
if (!boundHost) {
134120
throw new Error(

‎packages/platform-android/src/adb-shell-protocol.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { helperRuntimeSlot } from './adb-host.ts';
1+
import { runtimeSlot } from '@agent-device/host-kit/runtime-slots';
22
import type { AndroidAdbExecutor } from './adb-executor.ts';
33

44
/**
@@ -11,7 +11,7 @@ const FEATURE_PROBE_TIMEOUT_MS = 2_000;
1111

1212
const deviceExitStatusForwardingSlot = Symbol('androidDeviceExitStatusForwarding');
1313
const deviceExitStatusForwarding = (): Map<string, boolean> =>
14-
helperRuntimeSlot(deviceExitStatusForwardingSlot, () => new Map<string, boolean>());
14+
runtimeSlot(deviceExitStatusForwardingSlot, () => new Map<string, boolean>());
1515

1616
/**
1717
* Whether the exit status of a host `adb shell` child is the DEVICE command's exit status.

‎packages/platform-android/src/snapshot-helper-retirement.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { helperRuntimeSlot } from './adb-host.ts';
1+
import { runtimeSlot } from '@agent-device/host-kit/runtime-slots';
22
import { AppError } from '@agent-device/kernel/errors';
33
import type { DeviceInfo } from '@agent-device/kernel/device';
44
import { shellFragment } from '@agent-device/kernel/device-shell';
@@ -40,7 +40,7 @@ type PendingRetirement = {
4040

4141
const pendingRetirementsSlot = Symbol('androidPendingRetirements');
4242
const pendingRetirements = (): Map<string, PendingRetirement> =>
43-
helperRuntimeSlot(pendingRetirementsSlot, () => new Map<string, PendingRetirement>());
43+
runtimeSlot(pendingRetirementsSlot, () => new Map<string, PendingRetirement>());
4444

4545
export function getAndroidSnapshotHelperSessionDeviceKey(
4646
device: Pick<DeviceInfo, 'platform' | 'id'>,

‎packages/platform-android/src/snapshot-helper-session-lifecycle.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
* `snapshot-helper-session.ts`; they acquire through here and never reach the registry themselves.
88
*/
99
import type { AndroidAdbProcess } from './adb-executor.ts';
10-
import { helperRuntimeSlot, requireAndroidAdbHost } from './adb-host.ts';
10+
import { runtimeSlot } from '@agent-device/host-kit/runtime-slots';
11+
import { requireAndroidAdbHost } from './adb-host.ts';
1112
import type { DeviceInfo } from '@agent-device/kernel/device';
1213
import { AppError } from '@agent-device/kernel/errors';
1314
import { emitDiagnostic } from '@agent-device/host-kit/diagnostics';
@@ -96,14 +97,14 @@ export type AndroidSnapshotHelperSessionAcquisition = {
9697

9798
const sessionsSlot = Symbol('androidSnapshotHelperSessions');
9899
const sessions = (): Map<string, AndroidSnapshotHelperSession> =>
99-
helperRuntimeSlot(sessionsSlot, () => new Map<string, AndroidSnapshotHelperSession>());
100+
runtimeSlot(sessionsSlot, () => new Map<string, AndroidSnapshotHelperSession>());
100101
/**
101102
* Capture identity → when this process may spawn that helper build again after a failed start or a
102103
* failed session capture.
103104
*/
104105
const failedSessionsSlot = Symbol('androidSnapshotHelperFailedSessions');
105106
const failedSessions = (): Map<string, number> =>
106-
helperRuntimeSlot(failedSessionsSlot, () => new Map<string, number>());
107+
runtimeSlot(failedSessionsSlot, () => new Map<string, number>());
107108

108109
/**
109110
* Starts (or reuses) the session without capturing, so a helper-backed read that is not a snapshot

‎packages/platform-apple/src/core/runner-host.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
readHostCpuArchSync,
2424
} from '@agent-device/host-kit/process';
2525
import { Deadline, isEnvTruthy, retryWithPolicy } from '@agent-device/host-kit/retry';
26+
import { runtimeSlot } from '@agent-device/host-kit/runtime-slots';
2627

2728
import { withKeyedLock } from '@agent-device/kernel/keyed-lock';
2829

@@ -45,7 +46,6 @@ import {
4546
getAppleRunnerHostEnvironment,
4647
getRunnerDeviceClaimAuthorityProbe,
4748
getRunnerLeaseOwnerStateDir,
48-
runnerRuntimeSlot,
4949
} from './runner-owner-state.ts';
5050
import { buildSimctlArgsForDevice, simulatorAddressFor } from './simctl.ts';
5151
import {
@@ -121,6 +121,6 @@ export const appleRunnerHost: AppleRunnerHost = {
121121
resolveIosPhysicalDeviceControl,
122122
visitXmlPlistEntries,
123123
leaseOwnerStateDir: getRunnerLeaseOwnerStateDir,
124-
runnerRuntimeSlot,
124+
runnerRuntimeSlot: runtimeSlot,
125125
hasDeviceClaimAuthority: (device) => getRunnerDeviceClaimAuthorityProbe()?.(device) ?? false,
126126
};

‎packages/platform-apple/src/core/runner-owner-state.test.ts‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
11
import { expect, test } from 'vitest';
22
import { createLiveTestHostEnvironment } from '@agent-device/test-kit';
33
import { AppError } from '@agent-device/kernel/errors';
4+
import { createRuntimeStateHandle, runtimeSlot } from '@agent-device/host-kit/runtime-slots';
45
import { appleRunnerHost } from './runner-host.ts';
56
import {
6-
createAppleRunnerRuntimeState,
77
getRunnerLeaseOwnerStateDir,
8-
runnerRuntimeSlot,
8+
setAppleRunnerHostEnvironment,
99
setRunnerLeaseOwnerStateDir,
1010
} from './runner-owner-state.ts';
1111

1212
const SLOT = Symbol('test-slot');
1313

1414
test('each runtime keeps its own owner state and registries', () => {
15-
const first = createAppleRunnerRuntimeState(createLiveTestHostEnvironment());
16-
const second = createAppleRunnerRuntimeState(createLiveTestHostEnvironment());
15+
const first = createRuntimeStateHandle();
16+
const second = createRuntimeStateHandle();
17+
first.run(() => setAppleRunnerHostEnvironment(createLiveTestHostEnvironment()));
18+
second.run(() => setAppleRunnerHostEnvironment(createLiveTestHostEnvironment()));
1719

1820
first.run(() => {
1921
setRunnerLeaseOwnerStateDir(' /state/first ');
20-
runnerRuntimeSlot(SLOT, () => new Map<string, string>()).set('device', 'first');
22+
runtimeSlot(SLOT, () => new Map<string, string>()).set('device', 'first');
2123
});
2224
second.run(() => {
2325
setRunnerLeaseOwnerStateDir('/state/second');
2426
});
2527

2628
expect(first.run(() => getRunnerLeaseOwnerStateDir())).toBe('/state/first');
2729
expect(second.run(() => getRunnerLeaseOwnerStateDir())).toBe('/state/second');
28-
expect(
29-
first.run(() => runnerRuntimeSlot(SLOT, () => new Map<string, string>()).get('device')),
30-
).toBe('first');
31-
expect(second.run(() => runnerRuntimeSlot(SLOT, () => new Map<string, string>()).size)).toBe(0);
30+
expect(first.run(() => runtimeSlot(SLOT, () => new Map<string, string>()).get('device'))).toBe(
31+
'first',
32+
);
33+
expect(second.run(() => runtimeSlot(SLOT, () => new Map<string, string>()).size)).toBe(0);
3234
// Nothing leaked into the standalone state the code outside any runtime reads.
3335
expect(getRunnerLeaseOwnerStateDir()).toBeUndefined();
3436
});
3537

3638
test('disposing a runtime drops its registries', () => {
37-
const runtime = createAppleRunnerRuntimeState(createLiveTestHostEnvironment());
38-
runtime.run(() => runnerRuntimeSlot(SLOT, () => new Map([['device', 'owned']])));
39+
const runtime = createRuntimeStateHandle();
40+
runtime.run(() => runtimeSlot(SLOT, () => new Map([['device', 'owned']])));
3941

4042
runtime.dispose();
4143

42-
expect(runtime.run(() => runnerRuntimeSlot(SLOT, () => new Map<string, string>()).size)).toBe(0);
44+
expect(runtime.run(() => runtimeSlot(SLOT, () => new Map<string, string>()).size)).toBe(0);
4345
});
4446

4547
// A configuration read outside every composed runtime has no host to answer from; it refuses

‎packages/platform-apple/src/core/runner-owner-state.ts‎

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DeviceInfo } from '@agent-device/kernel/device';
22
import type { HostEnvironment } from '@agent-device/kernel/source-value';
3-
import { defineRuntimeSlots, type RuntimeStateHandle } from '@agent-device/host-kit/runtime-slots';
3+
import { runtimeSlot } from '@agent-device/host-kit/runtime-slots';
44

55
/**
66
* Daemon-owned device-claim arbitration probe: does the embedding process hold
@@ -12,48 +12,22 @@ import { defineRuntimeSlots, type RuntimeStateHandle } from '@agent-device/host-
1212
*/
1313
export type RunnerDeviceClaimAuthorityProbe = (device: DeviceInfo) => boolean;
1414

15-
/**
16-
* What one daemon runtime owns of the Apple runner: the lease-owner state directory and claim probe
17-
* its daemon set, and the registries (sessions, locks, recycle ledger) the runner modules keep in
18-
* {@link runnerRuntimeSlot}s. Kept in a dedicated tiny module so daemon startup can reach it
19-
* without loading the runner client's implementation closure; the runner package reads it through
20-
* its host port.
21-
*/
22-
const runnerRuntime = defineRuntimeSlots();
23-
24-
/**
25-
* The state of one runtime's Apple runner, with the host that runtime captured bound into it.
26-
* Root composition passes the same host its deps carry, so runner configuration reads and every
27-
* other configuration read of that runtime agree on one object (#3794).
28-
*/
29-
export function createAppleRunnerRuntimeState(host: HostEnvironment): RuntimeStateHandle {
30-
const state = runnerRuntime.createHandle();
31-
const bind = <T>(fn: () => T): T =>
32-
state.run(() => {
33-
runnerOwner().hostEnvironment = host;
34-
return fn();
35-
});
36-
return Object.freeze({
37-
run: bind,
38-
enter: () => {
39-
state.enter();
40-
runnerOwner().hostEnvironment = host;
41-
},
42-
dispose: () => state.dispose(),
43-
});
44-
}
45-
46-
/** One piece of runner state this daemon runtime owns, created on first use. */
47-
export const runnerRuntimeSlot = runnerRuntime.slot;
48-
4915
type RunnerOwner = {
5016
leaseOwnerStateDir: string | undefined;
5117
deviceClaimAuthorityProbe: RunnerDeviceClaimAuthorityProbe | undefined;
5218
hostEnvironment: HostEnvironment | undefined;
5319
};
20+
21+
/**
22+
* The Apple runner's per-runtime owner state: the lease-owner state directory and claim probe
23+
* its daemon set, and the host that daemon's composition bound (#3794). The runner's registries
24+
* (sessions, locks, recycle ledger) are separate slots the runner modules declare on the same
25+
* store through the runner host port. Kept in a dedicated tiny module so daemon startup can
26+
* reach it without loading the runner client's implementation closure.
27+
*/
5428
const RUNNER_OWNER_SLOT = Symbol('appleRunnerOwner');
5529
const runnerOwner = (): RunnerOwner =>
56-
runnerRuntimeSlot(RUNNER_OWNER_SLOT, (): RunnerOwner => ({
30+
runtimeSlot(RUNNER_OWNER_SLOT, (): RunnerOwner => ({
5731
leaseOwnerStateDir: undefined,
5832
deviceClaimAuthorityProbe: undefined,
5933
hostEnvironment: undefined,

‎packages/platform-apple/src/runner-owner-facade.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export {
2-
createAppleRunnerRuntimeState,
2+
getAppleRunnerHostEnvironment,
33
setAppleRunnerHostEnvironment,
44
setRunnerDeviceClaimAuthorityProbe,
55
setRunnerLeaseOwnerStateDir,

‎packages/platform-apple/src/runner/__tests__/runner-lease-runtime-host.test.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { expect, test, vi } from 'vitest';
22
import fs from 'node:fs';
33
import path from 'node:path';
44
import { mkdtempForTestSync } from './tmp-dir.ts';
5-
import { createAppleRunnerRuntimeState } from '../../core/runner-owner-state.ts';
5+
import { createTestRuntimeStateHandle } from '@agent-device/test-kit';
6+
import { setAppleRunnerHostEnvironment } from '../../core/runner-owner-state.ts';
67
import { currentRunnerLeaseOwnerToken, writeRunnerLease } from '../runner-lease.ts';
78

89
// Each composed runtime reads its runner configuration from the host it was composed with, not
@@ -14,14 +15,20 @@ test("each runner runtime resolves its host's runner lease directory, not the pr
1415
const root = mkdtempForTestSync('apple-runner-lease-host-');
1516
const dirA = path.join(root, 'a');
1617
const dirB = path.join(root, 'b');
17-
const runtimeA = createAppleRunnerRuntimeState({
18-
env: { ...process.env, AGENT_DEVICE_IOS_RUNNER_LEASE_DIR: dirA },
19-
cwd: process.cwd(),
20-
});
21-
const runtimeB = createAppleRunnerRuntimeState({
22-
env: { ...process.env, AGENT_DEVICE_IOS_RUNNER_LEASE_DIR: dirB },
23-
cwd: process.cwd(),
24-
});
18+
const runtimeA = createTestRuntimeStateHandle();
19+
const runtimeB = createTestRuntimeStateHandle();
20+
runtimeA.run(() =>
21+
setAppleRunnerHostEnvironment({
22+
env: { ...process.env, AGENT_DEVICE_IOS_RUNNER_LEASE_DIR: dirA },
23+
cwd: process.cwd(),
24+
}),
25+
);
26+
runtimeB.run(() =>
27+
setAppleRunnerHostEnvironment({
28+
env: { ...process.env, AGENT_DEVICE_IOS_RUNNER_LEASE_DIR: dirB },
29+
cwd: process.cwd(),
30+
}),
31+
);
2532

2633
runtimeA.run(() =>
2734
writeRunnerLease({

0 commit comments

Comments
 (0)