mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
34e8cbb7a2
* docs+ux: make device ownership discoverable end to end Complete the #1320 agent experience so 'busy? -> inspect -> choose or release' is discoverable from every surface an agent actually reads: - devices now projects the blocking claim owner per row (claimedBy with session and workspace, observe-policy projection; provably dead owners are excluded because the next open replaces them automatically), so an agent told a device is busy can pick a free one from the same listing. - help debugging gains a 'Device busy and ownership' section separating the two DEVICE_IN_USE flavors and their exact recoveries. - AGENTS.md documents both flavors; docs/agents/device-verification.md retires the last ps/kill recovery guidance in favor of device status, daemon stop --state-dir, and device release --stale (Stage 5 of #1320). - ADR-0010 no longer calls DEVICE_IN_USE 'the only retriable code' without naming the claim path's non-retriable override. - The rendered cross-worktree claim error gains a help-conformance quiz case binding (sample-output-device-claim-inspects-owner). - README points at device status / device release --stale. Part of #1320. * fix: key ownership projection by canonical device identity end to end Review findings on #2165: - blockingClaimOwnersByDevice keyed claims and inventory rows by bare device.id, so a live Android claim could project claimedBy onto an unrelated same-id Apple/Harmony/Vega row, with scan order picking the displayed owner. Both sides now use the canonical local device key (claim.deviceKey against canonicalLocalDeviceKey of the row's claim identity). The cross-family same-id regression was observed red against the bare-id keying. - The projection is now asserted across every hop the PR promises: client normalization preserves well-formed claimedBy and drops malformed ones, and the devices CLI formatter carries it through JSON data and renders the text line (MCP shares the same serialization).
65 lines
4.4 KiB
Markdown
65 lines
4.4 KiB
Markdown
# ADR 0010: Error system conventions
|
|
|
|
- Status: accepted
|
|
- Date: 2026-07-03
|
|
|
|
> **Amended by [ADR 0019](0019-request-bound-platform-runtime.md).** Existing normalization and
|
|
> cause-preservation rules remain accepted. When an operation and binding/resource cleanup both
|
|
> fail, the operation remains primary and cleanup is structured secondary diagnostic evidence;
|
|
> cleanup-only failure surfaces normally.
|
|
|
|
## Context
|
|
|
|
The error system is centralized in `src/kernel/errors.ts` (`AppError`, `normalizeError`,
|
|
`defaultHintForCode`, `retriableForErrorCode`) but has ~1,200 construction sites across the CLI,
|
|
daemon, and platform layers. A July 2026 audit (code sweep + iOS/Android dogfooding) found the
|
|
kernel sound while call-site quality drifted: internal validation thrown as bare `Error`
|
|
(surfacing as `UNKNOWN` with a misleading hint), semantically wrong codes (`COMMAND_FAILED` for
|
|
busy devices, `INVALID_ARGS` for expired server-side resources), missing hints on the most common
|
|
agent failures (selector/ref misses), and consumers that drop fields (MCP tool errors carried only
|
|
`message`). Nothing documented the conventions, so each new site re-decided them.
|
|
|
|
## Decision
|
|
|
|
1. **Every thrown error a user or agent can reach is an `AppError`.** Bare `throw new Error(...)`
|
|
is reserved for provably unreachable invariants. Input validation — including daemon-side
|
|
decoding of command input — throws `AppError('INVALID_ARGS', ...)`. Coercion of unknown caught
|
|
errors uses `asAppError(err, fallbackCode)`, which preserves the cause chain; do not hand-roll
|
|
`err instanceof AppError ? err : new AppError(...)`.
|
|
2. **Code selection.** Use the most specific `KnownAppErrorCode`; `COMMAND_FAILED` is for genuine
|
|
runtime failures of a well-formed request, never a catch-all for capability gaps
|
|
(`UNSUPPORTED_OPERATION`), contention (`DEVICE_IN_USE`, the only code retriable by default;
|
|
the cross-worktree device-claim path overrides it to `retriable: false`), or ambiguity
|
|
(`AMBIGUOUS_MATCH`). New codes are added to the union deliberately; machine-dispatchable
|
|
sub-classification rides in `details.reason` (the lease registry is the model).
|
|
3. **Hints answer "what should the agent run next".** A hint is required wherever the per-code
|
|
default from `defaultHintForCode` would mislead; it is omitted where the default suffices —
|
|
mass-adding boilerplate hints is worse than the default. Shared failure modes get shared hint
|
|
constants next to the code that detects them (`selectorFailureHint`, `STALE_REF_HINT` in
|
|
`packages/selectors/src/internal/resolve.ts`; `resolveIosDevicectlHint`; `bootFailureHint`), not copy-pasted
|
|
strings. Re-wraps preserve an existing hint rather than clobbering it.
|
|
4. **Wrapping external tool failures.** Prefer `exec.ts` errors as-is. A hand-rolled wrap of an
|
|
`allowFailure` result must carry `{ stdout, stderr, exitCode, processExitError: true }` so
|
|
`normalizeError` can surface the first meaningful stderr line instead of "tool exited with
|
|
code N".
|
|
5. **The wire error contract** is `code`, `message`, `hint`, `details` (redacted), `diagnosticId`,
|
|
`logPath`, plus optional typed signals `retriable` / `supportedOn` — the fields are absent
|
|
unless confidently known. Every consumer surface (CLI human, CLI `--json`, SDK, MCP, events
|
|
timeline) must carry code + message + hint at minimum; rehydration helpers
|
|
(`throwDaemonError`, `toDaemonHttpRpcError`) copy all fields, and `NormalizedError` hoists the
|
|
typed signals so `--json` consumers see them.
|
|
6. **Observability.** Failed requests always flush diagnostics: `diagnosticId` + ndjson `logPath`
|
|
accompany every error (daemon-side under the session state dir, client-side under
|
|
`~/.agent-device/logs`). Messages must not embed raw stderr dumps or secrets — structured
|
|
context belongs in `details`, which is redacted at normalize/write time.
|
|
|
|
## Consequences
|
|
|
|
- Agents can branch on `code`, retry on `retriable`, and follow `hint` without parsing prose;
|
|
wording can improve without breaking consumers (except strings pinned by help-text guarantees —
|
|
update those tests in lockstep).
|
|
- The known-code union stays small and meaningful; the widened `(string & {})` type still lets
|
|
runner/daemon codes pass through, so SDK consumers keep a `default` branch.
|
|
- New call sites have a documented bar: right code, contextual message naming the target, hint
|
|
only when the default misleads, cause preserved.
|