diff --git a/package.json b/package.json index 040fd12169..10195a80e6 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "test:fuzz-worker": "vitest run --project fuzz-worker", "test:integration:provider": "vitest run --project provider-integration", "test:integration:progress": "node --experimental-strip-types scripts/integration-progress.ts", - "test:integration:progress:check": "node --experimental-strip-types scripts/integration-progress.ts --check", + "test:integration:progress:check": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/integration-progress-model.test.ts && node --experimental-strip-types scripts/integration-progress.ts --check", "test:fixture-cache": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/ci/trusted-fixture-artifact.test.mjs", "test:fixture-fallback": "sh ./test/scripts/setup-fixture-app-fallback-smoke.sh", "test:output-economy": "vitest run --project output-economy", diff --git a/packages/capture-kit/src/snapshot/snapshot-freshness/index.ts b/packages/capture-kit/src/snapshot/snapshot-freshness/index.ts index 0837ecb5d5..63a1e04915 100644 --- a/packages/capture-kit/src/snapshot/snapshot-freshness/index.ts +++ b/packages/capture-kit/src/snapshot/snapshot-freshness/index.ts @@ -5,6 +5,7 @@ export { ANDROID_FRESHNESS_WINDOW_MS, androidFreshnessReason, buildSnapshotSignatures, + isLikelyStaleSnapshotDrop, isNavigationSensitiveAction, } from './android.ts'; export { diff --git a/packages/contracts/src/gesture-input.ts b/packages/contracts/src/gesture-input.ts index d8e2ffde30..d2294fd554 100644 --- a/packages/contracts/src/gesture-input.ts +++ b/packages/contracts/src/gesture-input.ts @@ -85,8 +85,6 @@ export type GesturePayload = | TransformGesturePayload | DragGesturePayload; -export type CoordinateGesturePayload = Exclude; - export function readGesturePayload(input: unknown): GesturePayload { const record = readRecord(input); const kind = readEnum(record, 'kind', GESTURE_KINDS); diff --git a/packages/contracts/src/interactor-operation-catalog.test.ts b/packages/contracts/src/interactor-operation-catalog.test.ts index 5da951594d..f0f5e530b7 100644 --- a/packages/contracts/src/interactor-operation-catalog.test.ts +++ b/packages/contracts/src/interactor-operation-catalog.test.ts @@ -16,27 +16,6 @@ const device = { const available = { available: true } as const; const unavailable = { available: false, reason: 'unsupported-device-kind' } as const; -test('binds every operation the facts admitted, driving the resolved interactor', async () => { - const back = vi.fn(async () => undefined); - const home = vi.fn(async () => undefined); - const resolveInteractor = vi.fn(async () => ({ back, home }) as unknown as Interactor); - const signal = new AbortController().signal; - - const operations = bindAdmittedLocalInteractorOperations({ - device, - signal, - resolveInteractor, - facts: { back: available, home: available }, - }); - - expect(operations.back).toBeTypeOf('function'); - expect(operations.home).toBeTypeOf('function'); - await operations.back?.({}); - await operations.home?.({}); - expect(back).toHaveBeenCalledOnce(); - expect(home).toHaveBeenCalledOnce(); -}); - test('skips an operation the facts refused', () => { const operations = bindAdmittedLocalInteractorOperations({ device, @@ -51,22 +30,6 @@ test('skips an operation the facts refused', () => { // The facts a caller passes are the ONLY source of truth for what binds — there is no separate, // caller-maintained operation list that could drift from them. -test('binds every admitted operation the facts name, not just the ones a caller had in mind', () => { - const operations = bindAdmittedLocalInteractorOperations({ - device, - signal: new AbortController().signal, - resolveInteractor: vi.fn(), - facts: { back: available, home: available, tvRemote: available }, - }); - - expect(operations.back).toBeTypeOf('function'); - expect(operations.home).toBeTypeOf('function'); - expect(operations.tvRemote).toBeTypeOf('function'); -}); - -// The walk list and the per-operation binder records are derived from one canonical tuple, so -// they can't drift at the type level; this pins the runtime behavior — every one of the seven -// operations binds when its fact admits it, not just the two or three most tests exercise. test('binds every one of the seven navigation operations when every fact admits it', async () => { const back = vi.fn(async () => undefined); const home = vi.fn(async () => undefined); diff --git a/packages/contracts/src/platform-runtime.ts b/packages/contracts/src/platform-runtime.ts index dff20619e5..8692c3c685 100644 --- a/packages/contracts/src/platform-runtime.ts +++ b/packages/contracts/src/platform-runtime.ts @@ -253,10 +253,6 @@ export type DeviceBindingRequest = Readonly<{ export type DeviceRuntimeGateway = Readonly<{ inspectFacts(device: DeviceInfo): Promise>; bind(request: DeviceBindingRequest): Promise>; - /** Complete close compatibility capability; unlike bind, this never admits a runtime facet. */ - getCloseShutdown?: () => Promise< - import('./device-shutdown-runtime.ts').DeviceShutdownCloseCapability - >; shutdown(): Promise; /** Package-coordinated durable lifecycle phases used for startup recovery and daemon shutdown. */ applicationLifecycle?: ApplicationLifecycleResourceLifecycle; diff --git a/packages/host-kit/src/internal/exec-kill-settle.test.ts b/packages/host-kit/src/internal/exec-kill-settle.test.ts index 98de96233d..f9701ad61e 100644 --- a/packages/host-kit/src/internal/exec-kill-settle.test.ts +++ b/packages/host-kit/src/internal/exec-kill-settle.test.ts @@ -11,7 +11,7 @@ import { type ExecBackgroundOptions, } from './exec.ts'; import { shellQuote } from '@agent-device/kernel/device-shell'; -import { sleep } from './timeouts.ts'; +import { sleep } from './retry.ts'; import { mkdtempForTestSync } from './tmp-dir.fixtures.ts'; // A direct child can hand our stdout/stderr pipes to a descendant, and `close` waits diff --git a/packages/host-kit/src/internal/exec.ts b/packages/host-kit/src/internal/exec.ts index 17ece9eb6a..d7adefd874 100644 --- a/packages/host-kit/src/internal/exec.ts +++ b/packages/host-kit/src/internal/exec.ts @@ -313,28 +313,28 @@ export async function resolveExecutableOverridePath( rawPath: string | undefined, envName: string, ): Promise { - const candidate = normalizeOverridePath(rawPath, envName, 'executable'); - if (!candidate) return undefined; - if (!(await isExecutablePath(candidate))) { - throw new AppError( - 'TOOL_MISSING', - `${envName} points to a missing or non-executable file: ${candidate}`, - { envName, path: candidate }, - ); - } - return candidate; + return await resolveOverridePath(rawPath, envName, 'executable', isExecutablePath); } export async function resolveFileOverridePath( rawPath: string | undefined, envName: string, ): Promise { - const candidate = normalizeOverridePath(rawPath, envName, 'file'); + return await resolveOverridePath(rawPath, envName, 'file', isFilePath); +} + +async function resolveOverridePath( + rawPath: string | undefined, + envName: string, + kind: 'executable' | 'file', + isUsable: (candidate: string) => Promise, +): Promise { + const candidate = normalizeOverridePath(rawPath, envName, kind); if (!candidate) return undefined; - if (!(await isFilePath(candidate))) { + if (!(await isUsable(candidate))) { throw new AppError( 'TOOL_MISSING', - `${envName} points to a missing or non-file path: ${candidate}`, + `${envName} points to a missing or non-${kind} ${kind === 'file' ? 'path' : 'file'}: ${candidate}`, { envName, path: candidate }, ); } @@ -647,14 +647,6 @@ function createStdinError( ); } -function createCommandCanceledError( - executable: string, - cmd: string, - args: readonly string[], -): AppError { - return createRequestCanceledError({ cmd, args, executable }); -} - function createTimeoutError( executable: string, cmd: string, @@ -768,7 +760,7 @@ function spawnRejectionError( err: Error, ): AppError { return abort.didAbort - ? createCommandCanceledError(executable, cmd, args) + ? createRequestCanceledError({ cmd, args, executable }) : createSpawnError(executable, cmd, args, err); } @@ -782,7 +774,7 @@ function commandCloseFailure( stdout: string, stderr: string, ): AppError | null { - if (abort.didAbort) return createCommandCanceledError(executable, cmd, args); + if (abort.didAbort) return createRequestCanceledError({ cmd, args, executable }); if (exitCode !== 0 && !allowFailure) { return createExitError(executable, cmd, args, exitCode, stdout, stderr); } diff --git a/packages/host-kit/src/internal/host-process.ts b/packages/host-kit/src/internal/host-process.ts index 316605cce9..f292b5065a 100644 --- a/packages/host-kit/src/internal/host-process.ts +++ b/packages/host-kit/src/internal/host-process.ts @@ -1,6 +1,6 @@ import type { ExecOptions, ExecResult } from './exec.ts'; import { runCmd, runCmdSync } from './exec.ts'; -import { sleep } from './timeouts.ts'; +import { sleep } from './retry.ts'; const PS_TIMEOUT_MS = 1_000; const HOST_PS_COMMAND = process.platform === 'win32' ? 'ps' : '/bin/ps'; diff --git a/packages/host-kit/src/internal/process-lock.ts b/packages/host-kit/src/internal/process-lock.ts index d5352530b4..7d0e3bc7ef 100644 --- a/packages/host-kit/src/internal/process-lock.ts +++ b/packages/host-kit/src/internal/process-lock.ts @@ -10,7 +10,7 @@ import { ownerIdentityMatches, type OwnerLiveness, } from './owner-identity.ts'; -import { sleep } from './timeouts.ts'; +import { sleep } from './retry.ts'; const OWNER_FILE_NAME = 'owner.json'; const DEFAULT_LOCK_TIMEOUT_MS = 30_000; diff --git a/packages/host-kit/src/internal/retry.ts b/packages/host-kit/src/internal/retry.ts index f6a529b401..8f143c5370 100644 --- a/packages/host-kit/src/internal/retry.ts +++ b/packages/host-kit/src/internal/retry.ts @@ -224,7 +224,7 @@ function computeDelay(base: number, max: number, jitter: number, attempt: number return Math.max(0, exp + (Math.random() * 2 - 1) * jitterAmount); } -function sleep(ms: number, signal?: AbortSignal): Promise { +export function sleep(ms: number, signal?: AbortSignal): Promise { return new Promise((resolve) => { if (signal?.aborted) { resolve(); diff --git a/packages/host-kit/src/internal/timeouts.ts b/packages/host-kit/src/internal/timeouts.ts deleted file mode 100644 index 421bda080a..0000000000 --- a/packages/host-kit/src/internal/timeouts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/packages/host-kit/src/retry.ts b/packages/host-kit/src/retry.ts index 7c112562c4..6f5496352a 100644 --- a/packages/host-kit/src/retry.ts +++ b/packages/host-kit/src/retry.ts @@ -1,2 +1,7 @@ -export { Deadline, type DeadlineClock, isEnvTruthy, retryWithPolicy } from './internal/retry.ts'; -export { sleep } from './internal/timeouts.ts'; +export { + Deadline, + type DeadlineClock, + isEnvTruthy, + retryWithPolicy, + sleep, +} from './internal/retry.ts'; diff --git a/packages/kernel/src/errors.ts b/packages/kernel/src/errors.ts index 56ed18b6bd..33d23c4572 100644 --- a/packages/kernel/src/errors.ts +++ b/packages/kernel/src/errors.ts @@ -222,27 +222,7 @@ function readDeviceList(value: unknown): Array<{ id: string; name: string }> { * error representation because clients immediately rehydrate this wire shape * into `AppError` before rendering or handling it. */ -export type DaemonError = { - code: string; - message: string; - cause?: ErrorCause; - hint?: string; - diagnosticId?: string; - /** Path on the DAEMON host. Meaningful to a local caller only (#1801). */ - logPath?: string; - /** Why no readable path is named; set by the client, never by the daemon. */ - logPathUnavailable?: string; - /** - * Additive locator (#1801) for the request diagnostics record `logPath` - * names, so a remote caller can fetch it over the daemon API instead of - * being handed a path on a filesystem it cannot read. - */ - diagnosticsRecord?: DiagnosticsRecordRef; - details?: ErrorWireDetails; - /** Additive retry and platform-support signals; absent when not derivable. */ - retriable?: boolean; - supportedOn?: string; -}; +export type DaemonError = NormalizedError; /** Rehydrate a daemon transport error into the error type used by local callers. */ export function throwDaemonError(error: DaemonError): never { diff --git a/packages/maestro/src/internal/__tests__/runtime-target-ranking.test.ts b/packages/maestro/src/internal/__tests__/runtime-target-ranking.test.ts index 3807a8b209..1afd6a852b 100644 --- a/packages/maestro/src/internal/__tests__/runtime-target-ranking.test.ts +++ b/packages/maestro/src/internal/__tests__/runtime-target-ranking.test.ts @@ -1,10 +1,6 @@ import { expect, test } from 'vitest'; import { attachSnapshotClickabilityEvidence } from '@agent-device/contracts/capture'; -import { - rankMaestroCandidates, - selectMaestroSnapshotMatch, - selectMaestroSnapshotMatches, -} from '../runtime-target-ranking.ts'; +import { rankMaestroCandidates, selectMaestroSnapshotMatch } from '../runtime-target-ranking.ts'; import { makeSnapshot } from './runtime-target-fixtures.ts'; test('typed target matching preserves snapshot read order', () => { @@ -226,22 +222,6 @@ test('authored index sorts by y then x with missing bounds last', () => { expect(selectMaestroSnapshotMatch(snapshot.nodes, 2)).toBeNull(); }); -test('nested childOf applies its own index before scoping the candidate', () => { - const snapshot = makeSnapshot([ - { index: 0, identifier: 'row', rect: { x: 0, y: 200, width: 200, height: 80 } }, - { index: 1, identifier: 'row', rect: { x: 0, y: 20, width: 200, height: 80 } }, - { index: 2, parentIndex: 0, label: 'Delete', rect: { x: 120, y: 220, width: 60, height: 40 } }, - { index: 3, parentIndex: 1, label: 'Delete', rect: { x: 120, y: 40, width: 60, height: 40 } }, - ]); - - expect( - selectMaestroSnapshotMatches(snapshot, { - text: 'Delete', - childOf: { id: 'row', index: 1 }, - }).map((node) => node.index), - ).toEqual([2]); -}); - test('target selection never fabricates a rectangle or promotes to an ancestor', () => { const snapshot = makeSnapshot([ { diff --git a/packages/maestro/src/internal/runtime-target-ranking.ts b/packages/maestro/src/internal/runtime-target-ranking.ts index a762fbe35a..291e3c8a50 100644 --- a/packages/maestro/src/internal/runtime-target-ranking.ts +++ b/packages/maestro/src/internal/runtime-target-ranking.ts @@ -66,15 +66,6 @@ export function matchMaestroCandidatesWithResolver( return { matches, parentMatched }; } -export function selectMaestroSnapshotMatches( - snapshot: SnapshotState, - selector: MaestroSelector, - platform?: MaestroPlatform, -): SnapshotNode[] { - const clickability = platform ? resolveMaestroClickability(snapshot, platform) : undefined; - return createMaestroResolver(snapshot, clickability).resolve(selector).indexed; -} - export function selectMaestroPositionMatches( snapshot: SnapshotState, relation: MaestroPositionRelation, diff --git a/packages/platform-android/src/observation.ts b/packages/platform-android/src/observation.ts index 68fbab4f5a..b93bb6bf17 100644 --- a/packages/platform-android/src/observation.ts +++ b/packages/platform-android/src/observation.ts @@ -1,5 +1,4 @@ import type { - AndroidBlockingDialogFocus, AndroidBlockingDialogObservation, AndroidObservationAdapter, AndroidObservationHost, @@ -8,14 +7,12 @@ import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-ru import type { DeviceInfo } from '@agent-device/kernel/device'; import { deviceShellArgv } from '@agent-device/kernel/device-shell'; import { AppError } from '@agent-device/kernel/errors'; +import { + ANDROID_FOCUS_MARKERS, + ANDROID_FOCUSED_WINDOW_MARKER, + readAndroidBlockingDialogFocus, +} from './app-parsers.ts'; -const FOCUSED_WINDOW_MARKER = 'mCurrentFocus=Window{'; -const FOCUS_MARKERS = [ - FOCUSED_WINDOW_MARKER, - 'mFocusedApp=AppWindowToken{', - 'mResumedActivity:', - 'ResumedActivity:', -] as const; const WINDOW_DUMPS: readonly (readonly string[])[] = [ ['dumpsys', 'window', 'windows'], ['dumpsys', 'window'], @@ -24,9 +21,10 @@ const ACTIVITY_DUMPS: readonly (readonly string[])[] = [ ['dumpsys', 'activity', 'activities'], ['dumpsys', 'activity'], ]; -const FOCUS_LINE = new RegExp(`(?:${FOCUS_MARKERS.map(escapeRegExp).join('|')})(.*)$`, 'gm'); -const ANR_TITLE = /\bApplication Not Responding:\s*([A-Za-z0-9_.]+)/i; -const PACKAGE_NAME = /\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\b/; +const FOCUS_LINE = new RegExp( + `(?:${ANDROID_FOCUS_MARKERS.map(escapeRegExp).join('|')})(.*)$`, + 'gm', +); const PERMISSION_PACKAGES = new Set([ 'com.android.permissioncontroller', 'com.google.android.permissioncontroller', @@ -113,7 +111,7 @@ async function readBlockingDialog( read: DumpReader = createDumpReader(host, device), ): Promise { for (const args of orderedDumps(device, 'blockingDialog')) { - const parsed = parseBlockingDialog(await read(args)); + const parsed = readAndroidBlockingDialogFocus(await read(args)); if (parsed.focus) return { status: 'dialog', focus: parsed.focus }; if (parsed.focusObserved) return { status: 'clear' }; } @@ -137,11 +135,11 @@ function orderedDumps(device: DeviceInfo, question: Question): readonly (readonl function recordSections(device: DeviceInfo, key: string, text: string): void { if (!text) return; const memo = everAnswered.get(device.id) ?? new Map(); - preservePositiveMemo(memo, `foreground ${key}`, markerPattern(FOCUS_MARKERS).test(text)); + preservePositiveMemo(memo, `foreground ${key}`, markerPattern(ANDROID_FOCUS_MARKERS).test(text)); preservePositiveMemo( memo, `blockingDialog ${key}`, - markerPattern([FOCUSED_WINDOW_MARKER]).test(text), + markerPattern([ANDROID_FOCUSED_WINDOW_MARKER]).test(text), ); everAnswered.set(device.id, memo); } @@ -160,55 +158,6 @@ function parseForeground(text: string): AppStateRuntimeResult | null { return null; } -function parseBlockingDialog(text: string): { - focusObserved: boolean; - focus: AndroidBlockingDialogFocus | null; -} { - let focusObserved = false; - const lines = text.split('\n'); - for (const marker of FOCUS_MARKERS) { - for (const line of lines) { - const index = line.indexOf(marker); - if (index === -1) continue; - if (marker === FOCUSED_WINDOW_MARKER) focusObserved = true; - const raw = line.trim(); - const segment = - line - .slice(index + marker.length) - .split('}')[0] - ?.trim() ?? ''; - const focus = parseDialogFocus(segment, raw); - if (focus) return { focusObserved, focus }; - } - } - return { focusObserved, focus: null }; -} - -function parseDialogFocus(segment: string, raw: string): AndroidBlockingDialogFocus | null { - const anrPackage = ANR_TITLE.exec(segment)?.[1]; - if (anrPackage) { - return { - package: anrPackage, - focusedWindow: `Application Not Responding: ${anrPackage}`, - raw, - }; - } - const normalizedSegment = segment.toLowerCase(); - if ( - !normalizedSegment.includes("isn't responding") && - !normalizedSegment.includes('is not responding') - ) { - return null; - } - const responding = segment.trim().replaceAll(/\s+/g, ' '); - const packageName = PACKAGE_NAME.exec(responding)?.[1]; - return { - ...(packageName ? { package: packageName } : {}), - focusedWindow: responding, - raw, - }; -} - function markerPattern(markers: readonly string[]): RegExp { return new RegExp(markers.map(escapeRegExp).join('|')); } diff --git a/packages/platform-apple/src/core/__tests__/install-artifact.fixtures.ts b/packages/platform-apple/src/core/__tests__/install-artifact.fixtures.ts deleted file mode 100644 index 93577bd1a6..0000000000 --- a/packages/platform-apple/src/core/__tests__/install-artifact.fixtures.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { runCmdSync } from '@agent-device/host-kit/command'; - -export async function createZipFixture( - archivePath: string, - directories: string[], - files: Array<{ source: string; target: string }> = [], -): Promise { - const staging = await fs.mkdtemp(path.join(path.dirname(archivePath), 'zip-fixture-')); - try { - for (const directory of directories) { - await fs.mkdir(path.join(staging, directory), { recursive: true }); - } - for (const file of files) { - const target = path.join(staging, file.target); - await fs.mkdir(path.dirname(target), { recursive: true }); - await fs.copyFile(file.source, target); - } - runCmdSync('zip', ['-qr', archivePath, '.'], { cwd: staging }); - } finally { - await fs.rm(staging, { recursive: true, force: true }); - } -} diff --git a/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts b/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts index 541f30b9eb..1991ff970a 100644 --- a/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts +++ b/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts @@ -3,7 +3,6 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { ensureBootedSimulator, markSimulatorBooted, - shutdownSimulator, SIMULATOR_BOOTED_MEMO_TTL_MS, wasSimulatorRecentlyObservedBooted, } from '../simulator.ts'; @@ -62,27 +61,6 @@ test('ensureBootedSimulator skips the state listing within the booted memo TTL', expect(countSimctlListCalls()).toBe(2); }); -test('shutdownSimulator invalidates the booted memo', async () => { - await ensureBootedSimulator(simulator); - expect(countSimctlListCalls()).toBe(1); - - await shutdownSimulator(simulator); - - await ensureBootedSimulator(simulator); - expect(countSimctlListCalls()).toBe(2); -}); - -test('shutdownSimulator forwards cancellation to the native simctl command', async () => { - const controller = new AbortController(); - - await shutdownSimulator(simulator, controller.signal); - - expect(mockRunXcrun).toHaveBeenCalledWith( - ['simctl', 'shutdown', simulator.id], - expect.objectContaining({ signal: controller.signal }), - ); -}); - test('markSimulatorBooted seeds the memo so the first boot check skips the listing', async () => { markSimulatorBooted(simulator); diff --git a/packages/platform-apple/src/core/simulator.ts b/packages/platform-apple/src/core/simulator.ts index 7ffd1b56d1..7317ada69a 100644 --- a/packages/platform-apple/src/core/simulator.ts +++ b/packages/platform-apple/src/core/simulator.ts @@ -56,10 +56,6 @@ export function markSimulatorBooted(device: DeviceInfo): void { simulatorBootedMemo.set(simulatorBootedMemoKey(device), true); } -function clearSimulatorBootedMemo(device: DeviceInfo): void { - simulatorBootedMemo.delete(simulatorBootedMemoKey(device)); -} - export function requireSimulatorDevice(device: DeviceInfo, command: string): void { if (device.kind !== 'simulator') { throw new AppError('UNSUPPORTED_OPERATION', `${command} is only supported on iOS simulators`); @@ -209,26 +205,6 @@ export async function ensureBootedSimulator( await openIosSimulatorApp({ signal: options.signal }); } -export async function shutdownSimulator( - device: DeviceInfo, - signal?: AbortSignal, -): Promise<{ - success: boolean; - exitCode: number; - stdout: string; - stderr: string; -}> { - clearSimulatorBootedMemo(device); - const args = buildSimctlArgsForDevice(device, ['shutdown', device.id]); - const result = await runXcrun(args, { allowFailure: true, timeoutMs: 15_000, signal }); - return { - success: result.exitCode === 0, - exitCode: result.exitCode, - stdout: result.stdout, - stderr: result.stderr, - }; -} - async function getSimulatorState(device: DeviceInfo, signal?: AbortSignal): Promise { const simctlArgs = buildSimctlArgsForDevice(device, ['list', 'devices', '-j']); const result = await runXcrun(simctlArgs, { diff --git a/packages/provision-kit/src/install-source.ts b/packages/provision-kit/src/install-source.ts index 6db5b72c21..7435a1466f 100644 --- a/packages/provision-kit/src/install-source.ts +++ b/packages/provision-kit/src/install-source.ts @@ -27,7 +27,6 @@ export type MaterializeInstallableOptions = { ) => boolean; installableLabel: string; signal?: AbortSignal; - downloadTimeoutMs?: number; }; export type MaterializedInstallable = { @@ -51,10 +50,7 @@ export async function materializeInstallablePath( ): Promise { const cleanupTasks: Array<() => Promise> = []; try { - const localSource = await materializeLocalSource(options.source, { - signal: options.signal, - downloadTimeoutMs: options.downloadTimeoutMs, - }); + const localSource = await materializeLocalSource(options.source, { signal: options.signal }); cleanupTasks.push(localSource.cleanup); const resolved = await resolveInstallableCandidate(localSource.localPath, { archivePath: undefined, @@ -89,7 +85,7 @@ function expandSourcePath(inputPath: string): string { async function materializeLocalSource( source: LocalInstallSource, - options?: { signal?: AbortSignal; downloadTimeoutMs?: number }, + options?: { signal?: AbortSignal }, ): Promise { if (source.kind === 'path') { return { @@ -117,19 +113,23 @@ async function downloadToTempFile( tempDir: string, url: string, headers?: Record, - options?: { signal?: AbortSignal; downloadTimeoutMs?: number }, + options?: { signal?: AbortSignal }, ): Promise { const requestSignal = options?.signal; if (requestSignal?.aborted) { throw createRequestCanceledError(); } - const timeoutMs = options?.downloadTimeoutMs ?? DEFAULT_SOURCE_DOWNLOAD_TIMEOUT_MS; - const timeoutSignal = AbortSignal.timeout(timeoutMs); + const timeoutSignal = AbortSignal.timeout(DEFAULT_SOURCE_DOWNLOAD_TIMEOUT_MS); const signal = requestSignal ? AbortSignal.any([requestSignal, timeoutSignal]) : timeoutSignal; try { return await downloadInstallSource({ tempDir, url, headers, signal }); } catch (error) { - throw classifyDownloadError(error, requestSignal, timeoutSignal, timeoutMs); + throw classifyDownloadError( + error, + requestSignal, + timeoutSignal, + DEFAULT_SOURCE_DOWNLOAD_TIMEOUT_MS, + ); } } diff --git a/scripts/__tests__/help-conformance-bench.test.ts b/scripts/__tests__/help-conformance-bench.test.ts index dfd9dbfe98..12c0289fdf 100644 --- a/scripts/__tests__/help-conformance-bench.test.ts +++ b/scripts/__tests__/help-conformance-bench.test.ts @@ -470,14 +470,6 @@ test('plan validator rejects a doubled && as an empty chain operand', async () = assert.equal(closeSegment.issues.length, 0); }); -test('plan validator still allows a quoted && to pass through a single segment unsplit', async () => { - const [single] = await validatePlanCommands([ - 'agent-device fill \'label="A && B"\' "value" --settle', - ]); - assert.equal(single.issues.length, 0); - assert.deepEqual(single.tokens, ['agent-device', 'fill', 'label="A && B"', 'value', '--settle']); -}); - test('case matchers score parsed tokens so shell quoting does not change results', async () => { const commands = [ 'agent-device open "com.example.shop"', diff --git a/scripts/check-affected/lockfile-install-sync.test.ts b/scripts/check-affected/lockfile-install-sync.test.ts index da6076bf12..994bb50be1 100644 --- a/scripts/check-affected/lockfile-install-sync.test.ts +++ b/scripts/check-affected/lockfile-install-sync.test.ts @@ -44,17 +44,6 @@ test('reports install-missing when a source checkout has no installed snapshot', }); }); -test('reports install-missing for a fresh worktree that has no node_modules directory at all', () => { - const root = mkdtempForTestSync('agent-device-lockfile-sync-fresh-worktree-'); - writeLockfile(root, 'lockfileVersion: 9.0\n'); - assert.equal(fs.existsSync(path.join(root, 'node_modules')), false); - - assert.deepEqual(checkLockfileInstallSync(root), { - status: 'out-of-sync', - reason: 'install-missing', - }); -}); - test('reports no-source-checkout when there is no pnpm-lock.yaml', () => { const root = mkdtempForTestSync('agent-device-lockfile-sync-packaged-'); fs.writeFileSync(path.join(root, 'package.json'), '{"name":"agent-device"}\n'); @@ -68,18 +57,3 @@ test('reports no-source-checkout even when an installed snapshot exists without assert.deepEqual(checkLockfileInstallSync(root), { status: 'no-source-checkout' }); }); - -test('checks each worktree against its own lockfile state', () => { - const worktreeA = mkdtempForTestSync('agent-device-lockfile-sync-worktree-a-'); - const worktreeB = mkdtempForTestSync('agent-device-lockfile-sync-worktree-b-'); - writeLockfile(worktreeA, 'lockfileVersion: 9.0\nimporters:\n .: {}\n'); - writeInstalledSnapshot(worktreeA, 'lockfileVersion: 9.0\nimporters:\n .: {}\n'); - writeLockfile( - worktreeB, - 'lockfileVersion: 9.0\nimporters:\n .:\n dependencies:\n newDep: 1.0.0\n', - ); - writeInstalledSnapshot(worktreeB, 'lockfileVersion: 9.0\nimporters:\n .: {}\n'); - - assert.deepEqual(checkLockfileInstallSync(worktreeA), { status: 'in-sync' }); - assert.deepEqual(checkLockfileInstallSync(worktreeB), { status: 'out-of-sync', reason: 'stale' }); -}); diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts index 3b335d66c1..ead87c347e 100644 --- a/scripts/check-affected/model.test.ts +++ b/scripts/check-affected/model.test.ts @@ -409,21 +409,4 @@ test('vitest-related delegates changed paths to Vitest instead of modeling proje ); }); -// Guards the catalog against reality, not fixtures: the self-test above uses a -// hand-built scripts map, so this resolves every catalog entry against the real -// package.json. A renamed/removed script fails here instead of -// leaving `pnpm check:affected` broken on the exact command the docs advertise. const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); - -test('catalog resolves against the real package.json', () => { - const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { - scripts?: Record; - }; - const scripts = pkg.scripts ?? {}; - for (const spec of CHECK_CATALOG) { - assert.doesNotThrow( - () => resolveCommand(spec, scripts, 'origin/main'), - `catalog entry "${spec.id}" must resolve against the real package.json`, - ); - } -}); diff --git a/scripts/check-affected/model.ts b/scripts/check-affected/model.ts index b98605822b..f1ed6994e2 100644 --- a/scripts/check-affected/model.ts +++ b/scripts/check-affected/model.ts @@ -27,73 +27,9 @@ import { deviceLaneOwnership } from './device-lanes.ts'; // the command-reference docs gate, which used to be reachable only as a workflow // job nothing in this repo could name. The run-gate action is the only way a CI lane // declares ownership, so `check:gate-manifest` fails when a registered check has no lane. -// Raw shell earns no ownership credit. -export type CheckId = - | 'format' - | 'lint' - | 'typecheck' - | 'test-app-typecheck' - | 'test-app-security' - | 'layering' - | 'di-seams' - | 'fallow' - | 'mcp-metadata' - | 'build' - | 'package' - | 'vitest-related' - | 'unit' - | 'unit-ci' - | 'coverage' - | 'provider-integration' - | 'integration-node' - | 'macos-coverage' - | 'ios-snapshot-differential' - | 'integration-progress' - | 'swift-runner-ios' - | 'swift-runner-macos' - | 'android-helpers' - | 'macos-helper' - | 'web-smoke' - | 'replay-compat' - | 'daemon-wire-compat' - // Tooling gates: each proves one of the checkers above still behaves. - | 'affected-selector' - | 'gate-manifest' - | 'gate-manifest-model' - | 'depgraph' - | 'tmpdir-leaks' - | 'tmpdir-leaks-model' - | 'coverage-model' - | 'wire-compat-model' - | 'production-exports' - | 'bundle-owner-files' - | 'freerange' - | 'fixture-cache' - | 'fixture-fallback' - | 'command-docs' - | 'agent-guidance' - | 'xctest-selection' - | 'packaged-runner-swift' - // Gates that drive their own runner — declared nowhere, registered here. - | 'maestro-conformance' - | 'maestro-differential' - | 'maestro-regenerate' - | 'fuzz-parsers' - | 'mutation' - | 'mutation-affected' - | 'mutation-check' - | 'mutation-model' - | 'concurrency-torture' - | 'replay-ios' - | 'replay-ios-device' - | 'replay-macos' - | 'replay-linux' - | 'linux-command-evidence' - | 'replay-android'; - -// The complete local check universe. A fail-open plan selects all of these; -// keep it in sync with the catalog in checks.ts (asserted by the self-test). -export const ALL_CHECKS: readonly CheckId[] = [ +// Raw shell earns no ownership credit. A fail-open plan selects all of these; keep it in sync +// with the catalog in checks.ts (asserted by the self-test). +export const ALL_CHECKS = [ 'format', 'lint', 'typecheck', @@ -155,7 +91,9 @@ export const ALL_CHECKS: readonly CheckId[] = [ 'replay-linux', 'linux-command-evidence', 'replay-android', -]; +] as const; + +export type CheckId = (typeof ALL_CHECKS)[number]; export type SelectionReason = { check: CheckId; diff --git a/scripts/ios-snapshot-benchmark/deep-button.test.ts b/scripts/ios-snapshot-benchmark/deep-button.test.ts index 1feb9b4b4d..b91619367f 100644 --- a/scripts/ios-snapshot-benchmark/deep-button.test.ts +++ b/scripts/ios-snapshot-benchmark/deep-button.test.ts @@ -3,7 +3,6 @@ import { test } from 'vitest'; import { assertInvalidShallowRuleFails, assertSafeFullRulePasses, - deepButtonFixtureEvidence, readDeepButtonFixtureArtifact, } from './deep-button.ts'; @@ -16,16 +15,6 @@ test('the checked-in fixture has a real 72-level ancestor chain', () => { assert.notDeepEqual(artifact.before.changedNode, artifact.after.changedNode); }); -test('the shallow control observes no change while the full tree changes', () => { - const evidence = deepButtonFixtureEvidence(); - assert.equal(evidence.artifact, 'deep-button-fixture.v1.json'); - assert.equal(evidence.depth, 72); - assert.equal(evidence.before.surfaceDigest, evidence.after.surfaceDigest); - assert.notEqual(evidence.before.fullDigest, evidence.after.fullDigest); - assert.ok(!evidence.after.surfaceNodeIds.includes(evidence.changedDescendant)); - assert.ok(evidence.after.fullNodeIds.includes(evidence.changedDescendant)); -}); - test('the planted invalid rule is red and the full rule is green', () => { assert.throws(assertInvalidShallowRuleFails, /changed descendant was omitted/); assert.doesNotThrow(assertSafeFullRulePasses); diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index b3382c6ba9..7c8e509807 100644 --- a/src/__tests__/cli-client-commands.test.ts +++ b/src/__tests__/cli-client-commands.test.ts @@ -828,31 +828,6 @@ test('wait keeps CLI bare text behavior through the typed client command API', a assert.equal(observed?.timeoutMs, 1500); }); -test('clipboard read keeps human text output through the typed client command API', async () => { - const client = createStubClient({ - installFromSource: async () => { - throw new Error('unexpected install call'); - }, - }); - client.command.clipboard = async () => ({ action: 'read', text: 'hello' }); - - const stdout = await captureStdout(async () => { - const handled = await tryRunClientBackedCommand({ - command: 'clipboard', - positionals: ['read'], - flags: { - json: false, - help: false, - version: false, - }, - client, - }); - assert.equal(handled, true); - }); - - assert.equal(stdout, 'hello\n'); -}); - test('keyboard status prints Android input ownership in human output', async () => { const client = createStubClient({ installFromSource: async () => { diff --git a/src/__tests__/cli-perf.test.ts b/src/__tests__/cli-perf.test.ts index a993f96504..0f4d5144f3 100644 --- a/src/__tests__/cli-perf.test.ts +++ b/src/__tests__/cli-perf.test.ts @@ -97,24 +97,6 @@ test('perf frames forwards frames area and prints focused frame summary', async assert.equal(result.stdout, 'Frame health: dropped 3.1% (12/390 frames) window 12s\n'); }); -test('perf frames sample forwards explicit sample action to daemon', async () => { - const result = await runCliCapture(['perf', 'frames', 'sample', '--json'], async () => ({ - ok: true, - data: { - metrics: { - fps: { - available: false, - reason: 'No frame data.', - }, - }, - }, - })); - - assert.equal(result.code, null); - assert.equal(result.calls[0]?.command, 'perf'); - assert.deepEqual(result.calls[0]?.positionals, ['frames', 'sample']); -}); - test('perf memory sample forwards memory area and prints compact memory summary', async () => { const result = await runCliCapture(['perf', 'memory', 'sample'], async () => ({ ok: true, diff --git a/src/__tests__/kernel/errors-code-policy.test.ts b/src/__tests__/kernel/errors-code-policy.test.ts index e04f2688d7..4845dd0295 100644 --- a/src/__tests__/kernel/errors-code-policy.test.ts +++ b/src/__tests__/kernel/errors-code-policy.test.ts @@ -200,19 +200,6 @@ test('defaultHintForCode falls back to the generic retry hint for a daemon/runne ); }); -test('defaultHintForCode never returns a falsy hint for any known code — printHumanError silently drops a falsy hint', () => { - // src/commands/output/error.ts's printHumanError only prints "Hint: ..." when - // `normalized.hint` is truthy, so an empty-string hint would silently vanish - // from CLI output rather than fail loudly. - for (const code of KNOWN_APP_ERROR_CODES) { - const hint = defaultHintForCode(code); - assert.ok(hint, `expected a non-empty hint for ${code}`); - } -}); - -// --- normalizeAgentDeviceError: the SDK-facing name for normalizeError --- -// (src/sdk/index.ts re-exports it under this name for public consumers.) - test('normalizeAgentDeviceError delegates to normalizeError, including the context argument', () => { const err = new AppError('SESSION_NOT_FOUND', 'gone'); const normalized = normalizeAgentDeviceError(err, { diff --git a/src/__tests__/kernel/errors-metadata.test.ts b/src/__tests__/kernel/errors-metadata.test.ts index 27f622d6b1..c6702e483b 100644 --- a/src/__tests__/kernel/errors-metadata.test.ts +++ b/src/__tests__/kernel/errors-metadata.test.ts @@ -48,16 +48,6 @@ test('normalizeError redacts and bounds a structured cause before putting it on assert.ok((normalized.cause?.message.length ?? 0) < cause.message.length); }); -test('normalizeError provides app discovery guidance for app-not-installed errors', () => { - const normalized = normalizeError( - new AppError('APP_NOT_INSTALLED', 'No package found matching "chat"'), - ); - assert.match( - normalized.hint ?? '', - /Run apps to discover the exact installed package or bundle id/i, - ); -}); - test('normalizeError lifts details.retriable to the top level and strips it from details', () => { const normalized = normalizeError( new AppError('COMMAND_FAILED', 'adb exited with code 1', { @@ -104,16 +94,6 @@ test('normalizeError preserves details.divergence verbatim through redaction/str assert.equal(normalized.details?.step, 2); }); -test('daemon-originated REPLAY_DIVERGENCE gets a default hint pointing at the report', () => { - const normalized = normalizeError( - new AppError('REPLAY_DIVERGENCE', 'Replay failed at step 1', {}), - ); - assert.match(normalized.hint ?? '', /details\.divergence/); -}); - -// --- stringDetail/booleanDetail/stripDiagnosticMeta: type-guard and -// empty-after-stripping contracts normalizeError relies on --- - test('normalizeError ignores a non-string details.diagnosticId/logPath/hint rather than surfacing the wrong-typed value', () => { const normalized = normalizeError( new AppError('COMMAND_FAILED', 'runner failed', { diff --git a/src/commands/capture/runtime/snapshot.ts b/src/commands/capture/runtime/snapshot.ts index fd1f2becb2..de306a185e 100644 --- a/src/commands/capture/runtime/snapshot.ts +++ b/src/commands/capture/runtime/snapshot.ts @@ -27,6 +27,7 @@ import { renderSnapshotQualityWarnings, truncatedCaptureWarning, } from '@agent-device/capture-kit/quality-warnings'; +import { isLikelyStaleSnapshotDrop } from '@agent-device/capture-kit/snapshot-freshness'; import { formatPostGestureOutcomeWarning } from '@agent-device/capture-kit/post-gesture-stability'; import { buildSnapshotVisibility } from '@agent-device/capture-kit/snapshot-visibility'; import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '@agent-device/contracts/android-system-surface-disclosure'; @@ -462,8 +463,3 @@ function formatFreshnessWarnings( } return freshness.reason === 'sharp-drop' ? [STALE_SNAPSHOT_DROP_WARNING] : []; } - -function isLikelyStaleSnapshotDrop(previousCount: number, currentCount: number): boolean { - if (previousCount < 12) return false; - return currentCount <= Math.floor(previousCount * 0.2); -} diff --git a/src/commands/command-contract.ts b/src/commands/command-contract.ts index f01c8f2612..8456ec0776 100644 --- a/src/commands/command-contract.ts +++ b/src/commands/command-contract.ts @@ -9,7 +9,6 @@ export type JsonSchema = { items?: JsonSchema; minItems?: number; maxItems?: number; - prefixItems?: readonly JsonSchema[]; oneOf?: readonly JsonSchema[]; not?: JsonSchema; enum?: readonly unknown[]; diff --git a/src/daemon/__tests__/interaction-surface-signature.test.ts b/src/daemon/__tests__/interaction-surface-signature.test.ts index bf0d5677b9..e2100079c3 100644 --- a/src/daemon/__tests__/interaction-surface-signature.test.ts +++ b/src/daemon/__tests__/interaction-surface-signature.test.ts @@ -8,13 +8,6 @@ import { discriminatingSurfaceChangedWithinRect, } from '../interaction-surface-signature.ts'; -test('areInteractionSurfaceSignaturesStable treats identical surfaces as stable', () => { - const before = buildInteractionSurfaceSignature(makeSnapshot('Inbox').nodes); - const after = buildInteractionSurfaceSignature(makeSnapshot('Inbox').nodes); - - assert.equal(areInteractionSurfaceSignaturesStable(before, after), true); -}); - test('areInteractionSurfaceSignaturesStable tolerates tiny rect drift', () => { const before = buildInteractionSurfaceSignature(makeSnapshot('Inbox', 100).nodes); const after = buildInteractionSurfaceSignature(makeSnapshot('Inbox', 100.4).nodes); diff --git a/src/daemon/__tests__/provider-device-admission.test.ts b/src/daemon/__tests__/provider-device-admission.test.ts index c90ea1b65a..c70424cadb 100644 --- a/src/daemon/__tests__/provider-device-admission.test.ts +++ b/src/daemon/__tests__/provider-device-admission.test.ts @@ -1,22 +1,15 @@ -import { afterEach, beforeEach, expect, test } from 'vitest'; +import { afterEach, expect, test } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { installProviderDeviceAdmission, isActiveProviderDevice, - providerDeviceAdmission, } from '../provider-device-admission.ts'; const providerDevice = { id: 'provider-1', name: 'Cloud iPhone' } as unknown as DeviceInfo; const localDevice = { id: 'local-1', name: 'iPhone 16' } as unknown as DeviceInfo; -let previous = providerDeviceAdmission(); - -beforeEach(() => { - previous = providerDeviceAdmission(); -}); - afterEach(() => { - installProviderDeviceAdmission(previous); + installProviderDeviceAdmission({ isActive: () => false }); }); test('an un-composed process treats every device as local', () => { diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-targets.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-targets.test.ts index 2e4138db01..ead2ca11dd 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-targets.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-targets.test.ts @@ -5,19 +5,6 @@ import { parseTouchTarget, } from '../interaction-touch-targets.ts'; -test('parseTouchTarget preserves ref fallback label through shared grammar', () => { - const parsed = parseTouchTarget(['@e4', 'Email field'], 'press'); - - expect(parsed).toEqual({ - ok: true, - target: { - kind: 'ref', - ref: '@e4', - fallbackLabel: 'Email field', - }, - }); -}); - test('parseTouchTarget trims ref fallback label', () => { const parsed = parseTouchTarget(['@e4', ' Email field '], 'press'); diff --git a/src/daemon/provider-device-admission.ts b/src/daemon/provider-device-admission.ts index 8d69a4850f..bae7032244 100644 --- a/src/daemon/provider-device-admission.ts +++ b/src/daemon/provider-device-admission.ts @@ -29,10 +29,6 @@ export function installProviderDeviceAdmission(admission: ProviderDeviceAdmissio installedProviderDeviceAdmission = admission; } -export function providerDeviceAdmission(): ProviderDeviceAdmission { - return installedProviderDeviceAdmission; -} - export function isActiveProviderDevice(device: DeviceInfo): boolean { return installedProviderDeviceAdmission.isActive(device); } diff --git a/src/daemon/server/daemon-idle-reap.test.ts b/src/daemon/server/daemon-idle-reap.test.ts index 9e7606dfb5..7bf2a18336 100644 --- a/src/daemon/server/daemon-idle-reap.test.ts +++ b/src/daemon/server/daemon-idle-reap.test.ts @@ -119,12 +119,6 @@ test('a repair-armed session that has already COMMITTED still blocks idle-reap ( assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), false); }); -test('an ordinary (non-repair) open session still blocks idle-reap', () => { - sessionStore.publish('default', makeSession()); - assert.equal(hasReapBlockingOpenSessions(sessionStore), true); - assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), false); -}); - test('a normal session alongside a reapable repair session still blocks idle-reap', () => { sessionStore.publish( 'default', diff --git a/test/output-economy/output-economy.baseline.test.ts b/test/output-economy/output-economy.baseline.test.ts index 53460c793f..37ee5ddafb 100644 --- a/test/output-economy/output-economy.baseline.test.ts +++ b/test/output-economy/output-economy.baseline.test.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, test } from 'vitest'; -import { defaultHintForCode, retriableForErrorCode } from '@agent-device/kernel/errors'; import { runCmdSync } from '@agent-device/host-kit/command'; import { findEconomyBudgetIncreases, @@ -181,17 +180,6 @@ describe('actionability and reliability floors', () => { }); expect(actual['not-settled.default.text']!.hints).toBe(1); }); - - test('policy-derived failures inherit hint and retry from production normalization', () => { - expect(rendered.errorPolicyNormalized).toMatchObject({ - code: 'DEVICE_IN_USE', - hint: defaultHintForCode('DEVICE_IN_USE'), - retriable: retriableForErrorCode('DEVICE_IN_USE'), - details: { reason: 'session-lock' }, - }); - expect(rendered.errorPolicyNormalized.retriable).toBe(true); - expect(rendered.errorPolicyNormalized.hint).toBeTruthy(); - }); }); describe('baseline-independent actionability floors', () => { diff --git a/test/wire-compat/ledger.json b/test/wire-compat/ledger.json index cae40d9ab3..e1c1b0cffa 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -41,7 +41,7 @@ "packages/kernel/src/contracts.ts#jsonRpcRequestSchema": "sha256:67e6b8a28b39a3883424a565ae7033c336dc2ea6b193e9b32bb98eb3e18dca7c", "packages/kernel/src/device.ts#PLATFORM_SELECTORS": "sha256:36e9da1cc660c0ddfb4cf52521f7f230341ff20e3ebda3405ba7c1799476c42d", "packages/kernel/src/device.ts#PlatformSelector": "sha256:61de3f003507ea2f53b396b0146c17670bf674a2db2132e19c462ca6c10cc8fd", - "packages/kernel/src/errors.ts#DaemonError": "sha256:d6cb31f516102d147bd4e4058ea4ace1d48584280f5b49205234c6dd5ada6e1c", + "packages/kernel/src/errors.ts#DaemonError": "sha256:b392b10f461b774f82252f8a5958781450b101329d43827abca10b6511b6d588", "packages/kernel/src/errors.ts#DiagnosticsRecordRef": "sha256:c5ef4185d01814fbfddcdcf797ce892f6ebfe1fd3c3fa73c7560ce2d3b6fce60", "packages/kernel/src/errors.ts#DispatchDisclosure": "sha256:60ec96fe63fb8756d7936187bef6226df1ed56d48ff03f254581b3685e3aa4ce", "packages/kernel/src/errors.ts#ErrorCause": "sha256:2f38679b0e9ec997fe8d94b5d5025404fd6ebb21f2c91ed2ab5d4133e578dfc4", @@ -231,8 +231,8 @@ }, { "declaration": "packages/kernel/src/errors.ts#DaemonError", - "digest": "sha256:d6cb31f516102d147bd4e4058ea4ace1d48584280f5b49205234c6dd5ada6e1c", - "rationale": "details.dispatched is a new OPTIONAL field inside details, which was already an open record: an older peer ignores the key and every existing field keeps its shape. It carries no | unknown for whether a failed interaction reached the device (the dispatch-disclosure golden table)." + "digest": "sha256:b392b10f461b774f82252f8a5958781450b101329d43827abca10b6511b6d588", + "rationale": "DaemonError is now declared as an alias of NormalizedError, whose members were already field-for-field identical (code, message, cause, hint, diagnosticId, logPath, logPathUnavailable, diagnosticsRecord, retriable, supportedOn, details). No wire byte changes; only the declaration text moved." }, { "declaration": "packages/kernel/src/errors.ts#NormalizedError",