Appearance
13 — Decision Log and Open Questions
NOTE
Part A records every decision this spec was required to make explicitly: the chosen v0.1 behavior, the alternatives considered, the trade-off, and whether the choice is considered settled (S) or revisitable with evidence (R). Full normative text lives at the linked sections — this table is the index, not a second source of truth. Part B lists what is genuinely open.
Part A — Decision log
| # | Question | Decision (v0.1) | Alternatives considered | Trade-off accepted | S/R | Spec |
|---|---|---|---|---|---|---|
| D1 | How is surfaceVersion generated/updated? | Per-registry monotonic counter (opaque string) + random surfaceId per page load; bumps on structural + pushed availability changes only; lazy when() drift doesn't bump | Content hash (expensive, no ordering); timestamp (collisions, clock skew); bump-on-everything (version storms) | Version ≠ full freshness: when is authoritative at invoke, not in the number | S | 03 §versioning |
| D2 | Can a registration change definition without deregistering? | No: structure frozen per registration; only enabled/availability/handlers are dynamic; structural change ⇒ re-register (new registrationId) | Mutable definitions + change events (staleness tokens become meaningless); versioned definitions per registration (complexity) | Occasional re-register churn in exchange for "reg_x always means the same thing" | S | 03 §lifecycle |
| D3 | React handlers changing every render? | Latest-ref pattern: handlers read at invocation time; no deps array, no re-registration, no version bump | Deps-array re-registration (churn + stale closures); forced useCallback (footgun) | Handlers are impossible to memo-freeze — deliberate | S | 04 §handler-freshness |
| D4 | How are ID collisions detected? | Key (plane,type,instanceId); structural dupes throw; runtime dupes → dead handle, first-wins (opt-in "replace"); cross-plane suffix heuristic warns | Throw always (crashes prod on transition races); last-wins (silent hijack) | Transient legit collisions (crossfades) need enabled:false or "replace" | S | 03 §collisions |
| D5 | Observations on demand or embedded in snapshot? | On demand only; snapshot is a pure sync catalog | Eager embedding (token waste, side-effect risk, forces async snapshot); opt-in embedded previews | Extra round-trip per read; revisit via OQ-8 hints | S | 03 §snapshot |
| D6 | How is surface size limited? | Hard caps on descriptions/meta/schemas; flat snapshot + priority; opt-in budget with explicit truncated marker (Experimental) | Hierarchical/query-navigable surface; automatic relevance ranking | Simple + predictable now; smart filtering deferred (OQ-4) | R | 03 §limits, §snapshot |
| D7 | Representation of partially bound inputs? | Agent-facing schema = original minus locked bound fields; boundFields metadata; values never in snapshots | Full schema + docs-only convention (models fill bound fields anyway); separate "context args" channel | Schema surgery must be correct (tested); top-level fields only (OQ-6) | S | 05 §binding |
| D8 | Can the agent override bound values? | Locked by default ⇒ INVALID_INPUT; explicit overridableFields opt-out (value becomes a default) | Overridable by default (reintroduces model guessing of authoritative UI state) | Some legit override flows need explicit opt-in | S | 05 §binding |
| D9 | Agent-visible vs internal metadata? | Two fields: meta (JsonValue, capped, serialized) vs internal (never serialized; policy/audit-only). Non-serialization is a tested invariant | Single bag + serializer allowlist (easy to leak by omission) | Slight duplication when a value is needed in both | S | 03 §definitions, 06 §minimization |
| D10 | Which policies client-side vs re-run server-side? | Client = UX + honesty (visibility, availability, confirmation UX, advisory rate/authz); server re-checks authn/authz/tenant/input/rate/approval on every domain call | Trusting client evaluation for "low-risk" ops (no such thing) | Double evaluation cost, by design | S | 05 §D10 table |
| D11 | Representing temporarily disabled capabilities? | In snapshot with available:false + unavailableReason; invoking ⇒ CAPABILITY_NOT_AVAILABLE {reason} | Hide when disabled (agent can't plan the enabling step) | Slightly larger snapshots | S | 01 §availability |
| D12 | Hide unauthorized capabilities or show as unavailable? | Authority hides, state discloses: authz failures hidden (and CAPABILITY_NOT_FOUND on invoke); state failures visible-disabled | Show-all-with-flags (leaks existence); hide-all (starves agent planning) | Policy authors must pick the right decision type | S | 06 §hide-vs-disable |
| D13 | Concurrent invocations? | Actions serialized FIFO per component instance, queue depth 2, overflow RATE_LIMITED{queue-full}; observations bounded (per-consumer 8 / global 32 / queue 8 — amended by D24; "unlimited" was a P0 bug) | Per-capability serialization (allows intra-instance races); global lock (needless coupling); unlimited (unbounded load/memory) | Cross-instance races possible (rare; accepted) | R | 03 §concurrency |
| D14 | Preventing double executions? | Corrected by D22: key = (consumerKey, invocationId) per registry + request fingerprint; join/cached only on fingerprint match; mismatch fails INVOCATION_CONFLICT; in-flight joining + terminal-result cache (excl. CONFIRMATION_REQUIRED, RATE_LIMITED, INVOCATION_CONFLICT); adapters reuse provider tool-call ids within their consumer namespace | Bare-invocationId cache (provider ids collide across consumers/pages → cross-request replay, the P0 bug); no dedupe; handler-level idempotency only | Bounded cache = dedupe window, not a guarantee forever | S | 03 §identity |
| D15 | Timeouts and cancellation? | Per-kind defaults (5/10/30 s), per-capability override; cooperative AbortSignal; TIMEOUT cached terminal (retry needs new id); external abort ⇒ CANCELLED | No timeouts (hangs); hard kill (impossible in JS) | Non-cooperative handlers can still leak work (logged as late-settlement) | S | 03 §concurrency |
| D16 | Navigation unmounts component mid-execution? | Amended by D23: non-navigation actions keep first-settle-wins (ok or COMPONENT_UNMOUNTED{mid-flight}, loser logged); navigation-effect actions settle on handler settlement only — unmount never overwrites a committed transition | Timing-dependent classification for navigation (the P0 bug: success reported as COMPONENT_UNMOUNTED); block unmount until settle (fights React); silent zombie handlers | Agent must treat mid-flight as "verify state" | S | 03 §concurrency, 07 §COMPONENT_UNMOUNTED |
| D17 | Event-ordering guarantees? | Total mutation order; post-mutation dispatch; coalesced surface-changed; queued (non-re-entrant) nested mutations; listener exceptions isolated; started<settled pairs | Fully async event bus (loses ordering); sync re-entrancy (corruption risk) | Listeners see slightly deferred nested mutations | S | 02 §guarantees |
| D18 | Input/output serialization? | JsonValue only; ISO dates; undefined stripped; dev-mode round-trip probe; output size cap fails loudly (output-too-large) | structuredClone semantics (not portable to wire); binary support (deferred) | No Dates/Maps/binary in payloads for now | S | 03 §serialization |
| D19 | JSON Schema subset? | Closed allowlist (2020-12 core + anyOf + internal $defs; no if/then, oneOf, allOf, patternProperties…); validated at registration; depth/size caps | Full JSON Schema (LLM + validator burden); provider-specific subsets (fragmentation) | Some Zod features won't convert; fails fast at registration | R | 03 §subset |
| D20 | Zod integration without hard dependency? | AgentSchema interface; fromStandardSchema(schema,{jsonSchema}) (validation via Standard Schema, conversion supplied by caller — Zod 4's z.toJSONSchema is one line); fromJsonSchema + minimal built-in validator as floor | Hard Zod dep in core (violates independence); auto-detect converters (magic, version-fragile) | One extra line per schema for Zod users (see the zs helper in 10) | S | 03 §schemas |
| D-eff | Do server-query/server-mutation belong to frontend actions? | No: server effects reserved to procedure references; view actions = local-state|navigation (enforced, PLANE_VIOLATION); indirect refetches remain local-state | Allow server effects on view actions (recreates shadow-RPC, kills the plane distinction) | Apps must define procedures for direct server work — that's the point | S | 01 §effects |
| D21 | Can input-aware policy/confirmation run before validated effective input exists? | No — P0 correction: two-stage policy API (onAuthorize pre-input, onInvoke post-input with effectiveInput only); confirmation decided at phase 6, evidence bound to canonical request digest {surfaceId, registrationId, capabilityId, consumerKey, effectiveInput, effect}; canonical JSON defined (sorted keys, -0→0, finite numbers, undefined omitted) | Single onInvoke with optional raw input (the contradiction: confirmation over unvalidated/unbound input); convention-only ordering (type system must forbid it) | Breaking Draft-API change; two onion chains instead of one | S | 18 §c1, 02 §pipeline, 06 §policy-pipeline |
| D22 | Invocation identity across consumers/adapters/pages? | Key (consumerKey, invocationId) per registry; fingerprint = fnv1a64(canonical request as issued); same key+different fingerprint → INVOCATION_CONFLICT fail-closed; anonymous → embedded:anonymous; adapters own per-instance consumer ids | Bare invocationId (collisions replay foreign results); fingerprint over effective input (impossible at phase 1 — recorded deviation from directive sketch, effective input is confirmation's digest instead); global registry-crossing cache (page reloads must isolate) | Conflict error surfaces adapter id-bugs loudly | S | 18 §c2, 03 §identity, 07 §INVOCATION_CONFLICT |
| D23 | What defines navigation success under owner unmount? | Handler-settlement authority for effect:"navigation": unregistration never settles the invocation; resolve→ok, reject+aborted-signal→CANCELLED, reject→EXECUTION_FAILED, else timeout/cancel; handler resolves on router acceptance | Injected AgentNavigationHost seam (second place to express one contract — designated fallback if real routers can't report acceptance in-handler); first-settle-wins (timing-dependent, the P0 bug) | Navigation handlers must settle on acceptance (sync routers trivially do) | S | 18 §c3, 01 §effects, 03 §concurrency |
| D24 | Are all runtime collections bounded? | Yes — new limits: observations per-consumer 8 / total 32 / queue 8 (overflow RATE_LIMITED{queue-full}, never the action queue); maxPendingConfirmations 32 (overflow fails closed, no record); defaults conservative until example-app measurements | Unlimited observations (P0: avoidable load/memory); expiring oldest pending confirmation (silently invalidates an approval being read) | Hard limits may need tuning with real data (revisitable defaults, not shape) | S | 18 §c4, 03 §concurrency, 03 §limits |
| D25 | Configurable action concurrency? | Contract specified (instance|capability|key|parallel{max}), default instance; implementation deferred to Phase C until base-queue race tests are green; not model-visible | Implement now (directive forbids: hardening, not slice prerequisite); never specify (adapters invent shapes) | Spec-ahead-of-code, tracked specified in conformance manifest | R | 18 §c5, 03 §concurrency |
| D26 | Default confirmation mode? | By declared topology: embedded→wait, remote→two-phase; createAgentToolset throws if neither topology nor confirmations given; WebMCP fixed two-phase; dispose deterministically settles pending waits | Global wait default (P0: holds remote runs open across human approvals); global two-phase (punishes the embedded majority case) | One more required option at toolset creation | S | 18 §c6, 09 §confirmation-topology, 03 §toolset |
Part B — Genuinely open questions
Each: context → options → current leaning → what it blocks.
OQ-1 · orpc-agent manifest derivation. The bridge needs {path → schemas, effect, requiresApproval} (05); the sideEffect → effect mapping is settled there. Per orpc-agent.dev, two concrete sources exist: a build-time export of the registry inventory (capabilities.capabilities()), or a bootstrap fetch of runtime.describe(surface, { actor, context }) — per-user, so server visibility policies already apply to what the frontend may reference. Leaning: describe()-based fetch, hand-written manifest as escape hatch. Still open: whether orpc-agent grows a dedicated frontend/contextual exposure surface alongside aiSdk/mcp/direct, which would make this a first-class export. Blocks: @agent-surface/orpc DX polish (M9), not v0.1. Decide before M9.
OQ-2 · WebMCP drift. navigator.modelContext shape, permissions, and lifecycle are unstable. Leaning: adapter tracks whatever ships; no core concession ever. Blocks: only @agent-surface/webmcp (v0.3).
OQ-3 · Cross-tab / multi-window surfaces. One agent, several tabs: merge surfaces? namespace by window? Options: SharedWorker aggregator; BroadcastChannel federation; explicit multi-registry consumer. Leaning: explicit consumer-side federation (no magic in core). Blocks: nothing pre-1.0.
OQ-4 · Relevance ranking / budgets beyond priority. Automatic "what matters now" filtering needs real usage data. Leaning: keep manual scope/priority; measure catalogs from the example app first. Blocks: large-app ergonomics.
OQ-5 · Contextual gating for server-side agents. Frontend when/bindings influencing a server-side agent's tool availability requires a session-sync protocol (frontend context → orpc-agent). Security-sensitive: the server must treat pushed context as hints, never authority. Leaning: design doc first; nothing in 0.x. Blocks: the server-agent topology only.
OQ-6 · Deep binding paths. bind covers top-level fields; nested (filters.city) needs path syntax + schema surgery on nested objects. Leaning: wait for a real case; workaround = restructure procedure input. Blocks: some binding shapes.
OQ-7 · i18n of descriptions/reasons. Agent-facing strings are English-authored today; user-facing confirmation summaries may need the app's locale. Leaning: summaries composable by host (requireConfirmation({summary})) is enough for v0.1; full i18n unscoped.
OQ-8 · Volatile snapshot hints. A capped, non-versioned snapshotHint ("42 rows, 3 selected") would help discovery without observation round-trips, but breaks "snapshot = pure catalog" and invites state leakage. Leaning: keep out; revisit with OQ-4 evidence.
OQ-9 · Meta-tools default threshold. When should the toolset auto-switch direct → meta? Leaning: never automatically in 0.x; explicit option only; measure model performance both ways.
OQ-10 · Streaming/subscribable observations. Agents polling readState during long operations is clumsy; a subscription channel conflicts with "minimal transfer" and most tool protocols. Leaning: out of 0.x; surfaceChanged hint + re-read covers the common case.
OQ-11 · Dedicated @agent-surface/zod sugar package. One line saved per schema vs. another package to version. Leaning: skip until D19 subset friction is measured; the zs() helper in userland is 3 lines.
OQ-12 · Confirmation UX for view: actions. confirmation:"required" on a local action (clear draft) uses the same evidence protocol as domain calls — correct but possibly heavy. Leaning: keep uniform (one protocol, one audit trail); revisit if UX friction shows up in the example app.