Files
callstack__agent-device/src/runtime.ts
T
Michał Pierzchała 33083b5a86 chore: codebase cleanup pass (dedup, types, dead code, cycles) (#420)
* refactor: remove dead code and unused exports

- Delete interaction-get.ts, interaction-is.ts, interaction-selector.ts (superseded by selector-runtime dispatchers)
- Remove handleWaitCommand and its private helpers from snapshot-wait.ts (replaced by dispatchWaitViaRuntime)
- Remove unused distanceFromSafeViewportBand/isRectWithinSafeViewportBand from rect-visibility
- Trim Linux platform barrel to only the re-export still in use (snapshotLinux)

* refactor: consolidate duplicated helpers

- Move sleep() to utils/timeouts.ts; remove 6 duplicate implementations
- Use existing isApplePlatform() helper for ios||macos checks (5 sites)
- Export trimRuntimeValue from runtime-hints; drop duplicate trimRuntimeString
- Merge normalizeTextSurfaceType/normalizeType into single text-surface helper
- Remove pointless isScrollableContainerType wrapper

* refactor: consolidate accidentally-duplicated type definitions

- SnapshotDiffLine/Summary: single definition in utils/snapshot-diff, re-exported from capture-snapshot
- FindLocator: single definition in utils/finders, re-exported from client-types
- JsonRpc envelope in http-server now uses JsonRpcRequestEnvelope/JsonRpcId from contracts
- Inline 'primary'|'secondary'|'middle' literals replaced with ClickButton (internal sites only)

* refactor: strengthen weak types and fix DEVICE_IN_USE downgrade bug

- Add toAppErrorCode() validator and DEVICE_IN_USE to AppErrorCode union
- Replace 'as any' casts on wire error codes with the validator (daemon-error, daemon-client, request-router, http-server)
- Type Metro worker payload as MetroTunnelResponseMessage
- Type xctestrun plist parsing with explicit partial schema
- Narrow 'details.stderr' via typeof checks on Android error paths
- Type runner-session parseRunnerResponse with RunnerResponsePayload

Bug fix: handler emitted 'DEVICE_IN_USE' but router cast silently downgraded to
'COMMAND_FAILED' because the code was missing from AppErrorCode. Clients can now
react to same-device contention.

* refactor: collapse redundant 'ignore' comments in empty catch blocks

- Replace 16 instances of 3-line catch { // ignore } with 1-line catch {}
- Remove one self-describing 'Re-export public API' comment

Specific 'ignore shutdown races' / 'ignore malformed pid files' style comments
that name concrete failure modes are kept.

* refactor: eliminate circular dependencies by extracting shared types to leaves

Resolves all 44 cycles reported by madge. Pattern throughout: extract shared
type into a leaf module; both producer and consumer import from the leaf;
original module re-exports for API stability.

New leaf type modules:
- src/runtime-contract.ts (AgentDeviceRuntime, CommandContext, ...)
- src/metro-types.ts (MetroRuntimeHints, MetroBridgeResult, ...)
- src/commands/runtime-types.ts (CommandResult, RuntimeCommand, ...)
- src/commands/diagnostics-types.ts
- src/cli/commands/router-types.ts (ClientCommandParams, ...)
- src/core/interactor-types.ts (Interactor, BackMode, ...)
- src/platforms/ios/runner-session-types.ts (RunnerSession)
- src/utils/screenshot-diff-region-types.ts (MutableDiffRegion)
- src/daemon/handlers/record-trace-types.ts

madge --circular now reports 0 cycles (was 44). No runtime behavior changes.

* fix: preserve wire error codes verbatim (addresses codex review)

The initial weak-types pass validated wire error codes against a closed
union, silently downgrading any unknown code to COMMAND_FAILED. This
dropped signals like AMBIGUOUS_MATCH that handlers emit and clients are
documented to handle (skills/agent-device/references/exploration.md).

- Widen AppErrorCode to 'KnownAppErrorCode | (string & {})' so autocomplete
  of known codes is preserved while any wire code flows through
- toAppErrorCode now preserves any non-empty code; fallback only when
  undefined or empty
- Add AMBIGUOUS_MATCH to KnownAppErrorCode (documented public code)
- Add test coverage for preservation and fallback behavior

* refactor: address review follow-ups

- Add DEVICE_IN_USE to the batch error taxonomy in exploration.md (now
  observable by clients after earlier fix, needs agent-facing guidance)
- Delete one-line src/platforms/linux/index.ts barrel; both consumers
  (core/dispatch, daemon/handlers/snapshot-capture) now import from
  platforms/linux/snapshot directly
- Replace inline { tenantId; runId; leaseId } shapes with MetroBridgeScope
  alias at client-types, metro, and cli/commands/connection-runtime
- Consolidate remaining inline setTimeout wrappers onto utils/timeouts.ts#sleep
  (12 files, ~18 sites). Left test files and the runtime-clock aware helper
  in commands/selector-read-utils alone. Also removes the local sleepMs
  helper from daemon-client.ts.

* refactor: address low-priority review follow-ups

- daemon-client RPC error path: stringify any non-null data.code instead
  of only forwarding strings. Preserves numeric codes from hypothetical
  future proxies/servers; for first-party daemon today this is a no-op
  since handlers already emit strings.
- errors.ts: expand AppErrorCode comment to call out the exhaustiveness
  tradeoff of the '(string & {})' widening for SDK consumers.
2026-04-17 13:22:46 +02:00

132 lines
3.8 KiB
TypeScript

import {
hasBackendEscapeHatch,
hasBackendCapability,
type BackendCapabilityName,
} from './backend.ts';
import { AppError } from './utils/errors.ts';
import { bindCommands, type BoundAgentDeviceCommands } from './commands/index.ts';
import type {
AgentDeviceRuntime,
AgentDeviceRuntimeConfig,
CommandPolicy,
CommandSessionRecord,
CommandSessionStore,
} from './runtime-contract.ts';
export type {
AgentDeviceRuntime,
AgentDeviceRuntimeConfig,
CommandClock,
CommandContext,
CommandPolicy,
CommandSessionRecord,
CommandSessionStore,
DiagnosticsSink,
} from './runtime-contract.ts';
export type AgentDevice = AgentDeviceRuntime & BoundAgentDeviceCommands;
export function createAgentDevice(config: AgentDeviceRuntimeConfig): AgentDevice {
const runtime: AgentDeviceRuntime = {
backend: config.backend,
artifacts: config.artifacts,
sessions: config.sessions ?? createMemorySessionStore(),
policy: config.policy ?? restrictedCommandPolicy(),
diagnostics: config.diagnostics,
clock: config.clock,
signal: config.signal,
};
return {
...runtime,
...bindCommands(runtime),
};
}
export function createMemorySessionStore(
records: readonly CommandSessionRecord[] = [],
): CommandSessionStore {
const sessions = new Map(records.map((record) => [record.name, cloneSessionRecord(record)]));
return {
get: (name) => cloneSessionRecord(sessions.get(name)),
set: (record) => {
sessions.set(record.name, cloneSessionRecord(record));
},
delete: (name) => {
sessions.delete(name);
},
list: () => Array.from(sessions.values(), (record) => cloneSessionRecord(record)),
};
}
function cloneSessionRecord(record: CommandSessionRecord): CommandSessionRecord;
function cloneSessionRecord(record: undefined): undefined;
function cloneSessionRecord(
record: CommandSessionRecord | undefined,
): CommandSessionRecord | undefined;
function cloneSessionRecord(
record: CommandSessionRecord | undefined,
): CommandSessionRecord | undefined {
if (!record) return undefined;
return {
...record,
...(record.snapshot ? { snapshot: structuredClone(record.snapshot) } : {}),
...(record.metadata ? { metadata: cloneMetadata(record.metadata) } : {}),
};
}
function cloneMetadata(metadata: Record<string, unknown>): Record<string, unknown> {
try {
return structuredClone(metadata) as Record<string, unknown>;
} catch {
return { ...metadata };
}
}
export function localCommandPolicy(overrides: Partial<CommandPolicy> = {}): CommandPolicy {
return {
allowLocalInputPaths: true,
allowLocalOutputPaths: true,
maxImagePixels: 20_000_000,
allowNamedBackendCapabilities: [],
...overrides,
};
}
export function restrictedCommandPolicy(overrides: Partial<CommandPolicy> = {}): CommandPolicy {
return {
allowLocalInputPaths: false,
allowLocalOutputPaths: false,
maxImagePixels: 20_000_000,
allowNamedBackendCapabilities: [],
...overrides,
};
}
export function assertBackendCapabilityAllowed(
runtime: Pick<AgentDeviceRuntime, 'backend' | 'policy'>,
capability: BackendCapabilityName,
): void {
if (!hasBackendCapability(runtime.backend, capability)) {
throw new AppError(
'UNSUPPORTED_OPERATION',
`Backend capability ${capability} is not supported by this backend`,
{ capability },
);
}
if (!runtime.policy.allowNamedBackendCapabilities.includes(capability)) {
throw new AppError(
'UNSUPPORTED_OPERATION',
`Backend capability ${capability} is not allowed by command policy`,
{ capability },
);
}
if (!hasBackendEscapeHatch(runtime.backend, capability)) {
throw new AppError(
'UNSUPPORTED_OPERATION',
`Backend capability ${capability} does not implement its escape hatch method`,
{ capability },
);
}
}