From 397673eb26954984c0b18cc4ac019d8b60a5a736 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 17 Aug 2026 17:38:03 +0200 Subject: [PATCH] docs(agent): document invocation context RPC and telemetry design --- packages/agent/docs/rpc.md | 397 +++++++++++++++++++++++++++++++ packages/agent/docs/telemetry.md | 252 ++++++++++++++++++++ 2 files changed, 649 insertions(+) create mode 100644 packages/agent/docs/rpc.md create mode 100644 packages/agent/docs/telemetry.md diff --git a/packages/agent/docs/rpc.md b/packages/agent/docs/rpc.md new file mode 100644 index 000000000..83613bafa --- /dev/null +++ b/packages/agent/docs/rpc.md @@ -0,0 +1,397 @@ +# Harness RPC Design Notes + +> **Status:** Design input, not a normative contract or implementation handoff. After the invocation-context interface migration lands, reconcile these notes with the actual interfaces, resolve the open decisions, and fold the accepted behavior into `harness.md` before creating a work package. + +## Goal + +Make the ordinary `AgentHarness`, `AgentLane`, `Session`, and `SessionTree` surfaces straightforward to expose through RPC with generic client/server machinery and small adapter-specific touch-ups. + +The core implementations remain transport-agnostic. They must not know about request IDs, serialization, sockets, remote references, trace carriers, disconnects, or subscription frames. + +```text +AgentHarness / AgentLane / Session / SessionTree + ↑ + server adapter + ↕ + wire protocol + ↕ + client proxies +``` + +Invocation context kills two integration problems at the boundary: + +- the adapter reconstructs the telemetry parent for the server invocation; +- the adapter maps request cancellation/disconnect to `context.signal`. + +The core sees only an ordinary explicit invocation context. + +## Non-goals + +Do not: + +- add RPC methods, reference types, request IDs, or transport state to real harness/session implementations; +- serialize `InvocationContext`, `AbortSignal`, `TelemetryContext`, hooks, tools, or callbacks; +- make the core depend on one transport or telemetry backend; +- use `AsyncLocalStorage` for parent discovery; +- hide durable cancellation behind an RPC disconnect; +- build per-method codecs for values already constrained to JSON. + +## Ordinary RPC contract + +An ordinary proxyable method should: + +- return a promise; +- accept only `JsonValue`-compatible business arguments; +- return a `JsonValue`-compatible ordinary result or `void`; +- optionally receive one required invocation context in the declared context position. + +`void` is represented by a successful response envelope without a result field. It does not require encoding `undefined` as a JSON value. + +Remote object references and subscriptions are protocol envelopes, not domain codecs. Methods returning non-JSON domain objects require an adapter projection or a change to a JSON result shape. + +Current examples requiring review include `SessionReader.getEntries()`, which returns a `Map`, and methods returning `AgentLane`, `SessionTree`, executable tools, or callback-bearing handles. The intended response is not a catalog of hand-written codecs. Use a remote reference, a JSON projection, or mark the member host-local. + +## Context position + +A generic proxy can support context first, context last, or no context by using a runtime brand or method metadata. Position does not solve method discovery by itself. + +Context first is preferable under a strict JSON argument contract: + +```ts +harness.prompt(context, text); +harness.prompt(context, text, images); +``` + +With context last, an optional argument before context requires an `undefined` array slot: + +```ts +harness.prompt(text, undefined, context); +``` + +`undefined` is not a `JsonValue`. Context first leaves optional business arguments trailing and omittable. The final interface migration must settle this before the signature becomes normative. + +## Contract checking + +Use both static and runtime checks. + +A mapped TypeScript assertion can reject ordinary members whose arguments/results are not JSON-compatible, are synchronous, or are non-method properties. Adapter-declared escape members are excluded from that ordinary check. + +Runtime transport boundaries must reject: + +- functions and symbols; +- `undefined` array entries; +- non-finite numbers; +- sparse arrays; +- cyclic data; +- class instances, `Map`, `Set`, `Date`, and other non-plain objects; +- symbol-keyed object state. + +JavaScript reflection can enumerate exposed implementation methods and validate values from executed calls. It cannot invent semantically valid arguments for arbitrary methods, so it cannot automatically execute every interface member. Static contract assertions and focused behavior fixtures remain necessary. + +TypeScript also permits concrete implementations to accept fewer parameters than their interfaces. Exact-signature type tests or explicit implementation inventories are needed during the context migration. + +## Generic client call + +Conceptually, the client proxy performs: + +```text +method invocation +→ find and remove branded InvocationContext +→ validate remaining arguments as JSON +→ start rpc.client span +→ inject trace carrier +→ allocate request ID +→ send target + method + arguments + trace carrier +→ on context.signal abort, send cancel(request ID) +→ decode JSON result or remote protocol envelope +``` + +A request frame can be transport-specific, but carries equivalent information: + +```ts +interface RpcRequest { + id: string; + target: ObjectReference; + method: string; + args: JsonValue[]; + contextPosition: "first" | "last" | "none"; + trace?: JsonValue; +} +``` + +`contextPosition` records where the server must insert its fresh local context. A trusted manifest may contain this metadata instead of accepting it from the wire. + +The context object, its values, signal, and telemetry implementation never appear in `args`. + +## Generic server call + +The server performs: + +```text +receive request +→ validate target, method, and JSON arguments +→ allocate request-local AbortController +→ extract trace carrier into local TelemetryContext +→ construct fresh local InvocationContext +→ start rpc.server span +→ derive context containing rpc.server span +→ insert context at declared position +→ invoke exposed real method or adapter override +→ validate result +→ return JSON/protocol envelope +``` + +A cancel frame aborts only the matching request controller. Disconnect aborts every active request and closes every subscription owned by that connection. + +`packages/protocol/src/rpc.ts` already separates a server implementation context from serialized arguments through `RpcImplementation`. It can inform this design, but the current client API has no invocation-context/cancellation/trace integration and the manifest model does not yet cover remote object references or subscriptions. + +## Remote object identity + +`AgentHarness`, `AgentLane`, `Session`, and `SessionTree` are stateful capabilities. They cross RPC boundaries as opaque references: + +```ts +interface ObjectReference { + readonly id: string; + readonly interface: "AgentHarness" | "AgentLane" | "Session" | "SessionTree"; +} +``` + +The protocol serializes a reserved JSON envelope. The client decodes it into another identity-only proxy. A proxy retains transport and remote identity, never an invocation context. + +Examples: + +- `harness.lane()` returns an `AgentLane` reference; +- successful `harness.createLane()` contains an `AgentLane` reference; +- `session.createLane()` returns a `SessionTree` reference; +- `harness.sessionTree` resolves to a linked `SessionTree` reference. + +References may be registry IDs or deterministic selectors. For example, `session.view(lane)` can be represented as `{ sessionId, lane }` without calling the server until a method on that tree is invoked. + +## Adapter escape hatch + +Most methods should use generic reflection/dispatch. Exceptional members are described in the server adapter, not implemented in the real harness/session classes. + +Conceptual exposure: + +```ts +function exposeHarness(server: RpcServer, harness: AgentHarness): ObjectReference { + return server.expose(harness, { + properties: { + sessionTree: remote((target) => target.sessionTree), + }, + results: { + lane: optionalRemote("AgentLane"), + createLane: resultRemote("AgentLane"), + }, + subscriptions: { + events: (target, publish) => installEventForwarder(target.events, publish), + }, + excluded: ["hooks", "getTools", "setTools", "runWhenIdle"], + }); +} +``` + +The exact descriptor API is open. It should support these operations: + +- invoke or decorate the corresponding real method; +- expose a returned object as another remote capability; +- project a readonly property; +- add a synthetic RPC projection when an existing callback/synchronous member cannot cross directly; +- create a subscription; +- exclude a host-local member. + +No descriptor, `$rpc` envelope, or transport concept belongs in `AgentHarness`, `Lane`, `Session`, or `SessionTree` implementations. + +## Surface classification + +### `SessionTree` + +Most asynchronous reads and writes are ordinary RPC after their argument/result types satisfy the JSON contract. Bound value/list payloads must themselves be JSON-compatible. + +A derived tree proxy retains only session/tree identity. It never captures the context used to obtain it. + +### `Session` + +Ordinary candidates include inherited tree/reader operations, lane creation, and close. + +Members requiring adaptation: + +- `metadata`: include a JSON snapshot with the reference or expose an async getter; +- `idGenerator`: host-local capability, not remotely exposed; +- `view(lane)`: synchronous remote-object return; use a deterministic client proxy or adapter projection; +- `mutate(callback)`: not ordinary RPC because it contains executable callback code and holds the session mutation line. + +If remote mutation is required, design a separate transaction protocol with explicit open/read/commit/close, timeout, cancellation, and disconnect behavior. Do not transparently serialize or reverse-call `Session.mutate()`. + +### `AgentLane` and `AgentHarness` + +Ordinary candidates include operation admission/drive/control, prompts, queue operations, navigation, configuration, inspection, idle waiting, lane lookup/creation, and close, subject to JSON-compatible types. + +Members requiring adaptation or exclusion: + +- returned `AgentLane` and `SessionTree` values become references; +- `runWhenIdle(callback)` is host-local; remote callers use `waitForIdle()`; +- executable tools and callback-bearing tool configuration remain host-local; +- constructor callbacks remain in the process hosting the harness; +- hooks remain host-local; +- events and watchers use subscriptions. + +Plain resources such as skill and prompt-template data may cross when their complete values are JSON-compatible and contain no executable behavior. + +## Hooks + +Do not proxy `hooks.on()` as an ordinary method. + +Hooks synchronously affect execution and may modify, block, decline, or terminate work. A remote hook requires explicit policy for: + +- registration lifetime; +- invocation IDs and duplicate delivery; +- ordering; +- timeout/deadline; +- disconnect; +- retry; +- fail-open versus fail-closed behavior; +- telemetry and cancellation for the reverse call. + +If remote hooks become a requirement, define a dedicated interceptor/reverse-RPC protocol. Do not make generic callback serialization part of the harness proxy. + +## Events and watchers + +Events are passive and one-way, so they map naturally to subscriptions. + +A subscription protocol needs frames equivalent to: + +```ts +type EventFrame = + | { type: "subscribe"; requestId: string; target: ObjectReference; filter?: JsonValue } + | { type: "subscribed"; requestId: string; subscriptionId: string } + | { type: "event"; subscriptionId: string; sequence: number; value: JsonValue; trace?: JsonValue } + | { type: "ack"; subscriptionId: string; sequence: number } + | { type: "unsubscribe"; subscriptionId: string }; +``` + +The server adapts host-local `events.on()` into this protocol. The real event bus remains unaware of RPC. + +Each event frame carries trace metadata from the context that emitted the event. The client reconstructs a fresh event-delivery context and invokes local listeners under that parent. It does not receive the server's `InvocationContext` object or arbitrary context values. + +Subscriptions are ongoing invocation-scoped resources. They may retain their creation context and own cancellation controller; ordinary harness/session proxies may not. Aborting the subscription context or disconnecting sends/unconditionally performs unsubscribe. + +The transport must buffer events arriving between server subscription installation and client reference decoding. Sequence numbers plus acknowledgements or a bounded flow-control policy prevent unbounded memory growth. + +`watch()` and `watchSession()` additionally require a race-free snapshot handshake: + +```text +server installs watcher in buffering mode +→ server captures snapshot +→ server sends subscription ID + snapshot +→ server starts buffered/future event delivery +→ client returns local WatchHandle +``` + +This preserves the current snapshot-plus-buffer semantics without serializing listener callbacks. + +A future client facade may reconstruct the familiar local `Events`/`WatchHandle` experience on top of this protocol. That facade lives in the RPC client package. + +## Request cancellation + +The adapter maps each invocation signal to one request ID: + +```text +client signal abort +→ cancel(request ID) +→ server request AbortController aborts +→ reconstructed context.signal aborts +``` + +Pre-aborted calls must not start server work. Disconnect aborts all active request controllers for that connection. + +This is process-local invocation cancellation. It must not call `requestAbort()` or write durable `cancel_requested` automatically. + +## `drive()` concurrency + +RPC does not decide whether a `drive()` caller installs execution or joins an existing process-local owner. The core lane arbitration decides that after receiving the reconstructed context. + +Every concurrent RPC has an independent signal and telemetry parent. Never combine every joiner's signal into the active execution signal. A canceled joiner must cancel only its own wait. + +The runtime must choose one execution ownership policy: + +- installer-owned execution; +- caller leases, where execution stops only after the last waiter leaves; +- harness-owned execution independent of caller lifetime. + +The adapter only maps disconnect to the affected invocation signal. See `telemetry.md` for joiner spans and the durable-cancellation distinction. + +## Telemetry across RPC + +RPC telemetry follows: + +```text +client caller +└─ rpc.client + └─ rpc.server + └─ harness/session span +``` + +The client injects trace metadata from `rpc.client`; the server extracts a new local parent before `rpc.server`. No telemetry object crosses the wire. + +A joiner's `rpc.server`/`drive.join` spans remain under the joiner's incoming trace. They correlate with active execution by lane, operation ID, execution ID, and eventually span links. They cannot become a second parent of already-running execution. + +Event frames independently inject the source event context so client-side event processing remains related to the operation that emitted the event rather than the subscription-establishment call. + +## Security and lifecycle + +The server exposure layer must: + +- allowlist methods or derive a trusted manifest rather than reflecting arbitrary property names from untrusted input; +- validate all request arguments before invocation and results before response; +- reserve and validate protocol-envelope keys; +- scope object references and subscriptions to the appropriate connection/authorization domain; +- release references, active requests, and subscriptions on disconnect; +- prevent clients from selecting arbitrary server-side context placement or internal implementation members; +- treat telemetry recording failures as passive and business-neutral. + +Authentication, authorization, and tenant metadata may be reconstructed as typed local context values by the server adapter. Client-supplied arbitrary context values are not trusted and do not cross by default. + +## Interface migration + +The initial context change is scaffolding, not proof of RPC behavior. + +`TODO_CONTEXT` marks a caller whose real parent is not yet threaded. `BACKGROUND_CONTEXT` is reserved for intentional roots. Temporary implementation shims may ignore context to make the repository compile, but they must not be described as cancellation or telemetry support. + +Because the superseded harness implementation has been removed, all eventual semantics must target the current harness runtime. Old-runtime propagation changes are not implementation evidence. + +## Required tests for the later handoff + +- static rejection of non-JSON ordinary interface members; +- runtime rejection of functions, cycles, non-finite numbers, sparse arrays, and class instances; +- context is removed from serialized arguments and reconstructed server-side; +- both supported context positions or the selected final position; +- ordinary transparent method calls; +- nested remote references and stable proxy identity; +- adapter override invokes/decorates the real method; +- property and synchronous remote-object projections; +- unknown target/method and malformed envelope rejection; +- pre-aborted request starts no server work; +- one request cancellation does not abort another; +- disconnect aborts requests and closes subscriptions; +- client/server telemetry parentage; +- malformed/missing trace carrier degrades safely; +- event subscription establishment race; +- event source telemetry reconstruction; +- unsubscribe, disconnect, ordering, buffering, and backpressure; +- snapshot plus buffered events has no observation gap; +- drive joiner cancellation does not cancel shared execution; +- invocation cancellation writes no durable cancellation state. + +## Open decisions before `harness.md` + +- final context type, field names, brand, and argument position; +- whether all ordinary results must be JSON-compatible or some interfaces need RPC projections; +- static contract-checking API and test location; +- object-reference ownership, lifetime, and nested result encoding; +- exact adapter descriptor/override API; +- event flow-control and reconnection policy; +- whether remote transaction or hook protocols are required at all; +- telemetry carrier adapter ownership; +- drive ownership policy under multiple callers; +- exact package boundaries for generic RPC, harness adapters, and protocol schemas. diff --git a/packages/agent/docs/telemetry.md b/packages/agent/docs/telemetry.md new file mode 100644 index 000000000..75ba195d6 --- /dev/null +++ b/packages/agent/docs/telemetry.md @@ -0,0 +1,252 @@ +# Invocation Context and Telemetry Design Notes + +> **Status:** Design input, not a normative contract or implementation handoff. After the invocation-context interface migration lands, reconcile these notes with the actual types, resolve the open decisions, and fold the accepted design into `harness.md`. `telemetry-schema.md` remains the generated reference for span names and attributes. + +## Goal + +`Session`, `SessionTree`, `AgentLane`, and `AgentHarness` must receive invocation-scoped control data explicitly. The same receiver may serve concurrent local callers or RPC clients, so it cannot retain a mutable or default caller context. + +The invocation context must solve two related problems without `AsyncLocalStorage`: + +1. preserve correct telemetry parentage through concurrent asynchronous work; +2. carry the `AbortSignal` that an RPC adapter can map to request cancellation. + +This work must reuse `@earendil-works/pi-telemetry`. It must not introduce another span abstraction. + +## Target context model + +The final names still need reconciliation with the interface migration. The intended semantics are: + +```ts +interface InvocationContext { + readonly signal: AbortSignal; + readonly telemetry: TelemetryContext; + value(key: ContextKey): T | undefined; +} +``` + +A context is immutable. Derivation creates a parent-linked layer: + +```ts +const requestContext = withSignal(parentContext, requestSignal); +const spanContext = withTelemetry(requestContext, span); +const tenantContext = withContextValue(spanContext, tenantKey, tenantId); +``` + +Required properties: + +- `signal` is always present. The background context uses a never-aborted signal rather than `undefined`. +- `telemetry` is always present. The background context uses `NOOP_TELEMETRY_CONTEXT`. +- typed values use symbol identity and immutable copy-on-write layers; +- context values are cross-cutting request metadata, not business dependencies; +- contexts, signals, telemetry objects, and backend-native span objects are never durable data. + +Suitable typed values include request IDs, authenticated principals, tenant IDs, and diagnostic metadata. Storage, models, tools, durable state, and business payloads do not belong in the context. + +## Receiver ownership + +Shared receivers retain identity and durable/process state, not invocation context: + +```text +AgentHarness receiver ── no caller context +AgentLane receiver ── no caller context +Session receiver ── no caller context +SessionTree receiver ── no caller context +``` + +Every invocation supplies its own context. This prevents concurrent callers from overwriting each other's telemetry parent or cancellation signal. + +A process-local object representing one ongoing invocation may retain its derived context. Examples are an active drive task or an event subscription. This is different from storing a default context on the shared harness or session receiver. + +`AgentHarnessOptions.telemetryContext` must disappear. A harness-level default cannot represent two concurrent callers with different parents. + +## Existing typed telemetry remains authoritative + +The design retains: + +- `TelemetryContext` and `TelemetrySpan`; +- callback-owned span lifetime; +- `AI_TELEMETRY_SCHEMA` and `HARNESS_TELEMETRY_SCHEMA`; +- typed span names, start attributes, completion attributes, and events; +- `startAiSpan()`, `startHarnessSpan()`, and `createTypedSpanStarter()`; +- adapter conformance behavior. + +Starting a harness span must derive the invocation context passed to lower work: + +```ts +return startHarnessSpan( + context.telemetry, + "pi.harness.run", + attributes, + async (span) => { + const runContext = withTelemetry(context, span); + return runDrive(runContext); + }, +); +``` + +A helper may package this pattern by giving its callback both the typed span and the derived invocation context. It still delegates to the existing `TelemetryContext.startSpan()` contract. + +Do not mutate a context to install an active span. Do not use a process-global or receiver-global current span. + +## Concurrent parentage + +Explicit propagation supports concurrent sibling calls: + +```ts +await parent.telemetry.startSpan({ name: "caller" }, async (callerSpan) => { + const callerContext = withTelemetry(parent, callerSpan); + await Promise.all([ + laneA.drive(callerContext, optionsA), + laneB.drive(callerContext, optionsB), + ]); +}); +``` + +Each invocation derives its own child context. Nested work receives the child belonging to that invocation. Correct parentage does not depend on promise scheduling or ambient state. + +Tests must cross concurrent branches deliberately so accidental receiver-level context is visible. A sequential parent/child test is insufficient. + +## Callbacks, hooks, and events + +Host-local callbacks invoked as part of an operation receive the current invocation context: + +```ts +handler(event, context); +tool.execute(context, invocation, arguments); +mutation(mutator, context); +``` + +A hook span derives a child context before invoking the hook. Hook-triggered lower work therefore remains under `pi.harness.hook`. + +Events preserve the context that caused each event. A buffered event watcher must buffer `{ event, context }`, not only `event`; otherwise delayed delivery is parented to the wrong caller. Each listener invocation starts `pi.harness.event_handler` from the event context and passes the resulting child context to the listener. + +Event registration itself is host-local configuration and has no operation parent. Event delivery has the operation parent. + +Session commits similarly start `pi.session.write` from the invocation that issued the transaction. The mutation callback and its sole commit continue to use the same explicit invocation lineage unless they deliberately derive child spans. + +## Drive execution and joiners + +Several callers may call `drive()` for the same durable operation. Arbitration decides which call installs process-local execution and which calls join it. This is a core runtime concern, not an RPC concern; concurrent local callers have the same issue. + +One active execution has one telemetry parent. It cannot be reparented when another caller joins. + +```text +installer caller +└─ drive.execute + └─ provider/tool work + +joiner caller +└─ drive.join +``` + +A joiner span describes that caller's wait. It carries at least lane name, durable operation ID, and a process-local execution ID. It ends with an outcome such as `settled`, `caller_cancelled`, `execution_stopped`, or `harness_closed`. + +The joiner must not overwrite the active execution context. Correlate the two spans using operation/execution attributes. Telemetry span links would model this relationship better, but the current telemetry contract has no links. Adding links is an optional telemetry-package design question, not a reason to invent multiple parents. + +Distributed traces permit an execution span to outlive the installer RPC span. Parent and child spans may overlap or settle in either order once the child has started. + +## Invocation cancellation versus durable cancellation + +An aborted invocation signal is process-local control. It does not mean that durable cancellation was requested. + +```text +context.signal aborts +→ stop or detach process-local waiting/work according to drive ownership policy +→ do not write cancel_requested +→ preserve a valid durable restart point +``` + +Only `requestAbort()`/`abort()` writes durable `cancel_requested` and permits durable aborted settlement. + +The runtime must track the stop cause instead of interpreting every aborted provider response as durable cancellation: + +```ts +type ExecutionStopCause = + | "no_drive_waiters" + | "invocation_cancelled" + | "harness_closed" + | "durable_cancel_requested"; +``` + +Only `durable_cancel_requested` may normalize and commit a durable aborted outcome. An invocation/disconnect abort must not produce an assistant `stopReason: "aborted"` settlement while durable control remains `running`; that path would violate the durable state machine. + +The drive ownership policy remains to be finalized: + +1. **Installer-owned:** the installer signal controls execution; joiner signals control only their waits. +2. **Caller leases:** every drive caller owns a wait lease; execution is aborted process-locally only when the last lease disappears. +3. **Harness-owned:** drive execution survives all caller disconnects until durable settlement, durable cancellation, close, or fault. + +Signals from unrelated joiners must never be combined with `AbortSignal.any()` and attached directly to shared execution. One canceled joiner cannot cancel every other caller. + +## RPC trace propagation + +Client and server spans can belong to one distributed trace: + +```text +caller +└─ rpc.client + └─ rpc.server + └─ harness/session operation +``` + +The client does not serialize `TelemetryContext`. It injects a backend-neutral trace carrier from the `rpc.client` span. The server extracts that carrier into a fresh local `TelemetryContext` and starts `rpc.server` from it. + +A transport-facing adapter boundary is required: + +```ts +interface TelemetryPropagation { + inject(context: TelemetryContext): JsonValue | undefined; + extract(carrier: JsonValue | undefined): TelemetryContext; +} +``` + +A production implementation may use W3C `traceparent`/`tracestate`. The current telemetry package has no carrier injection/extraction API, so the accepted design must decide whether this adapter belongs in the telemetry package, RPC infrastructure, or a backend integration package. It must still reuse the existing `TelemetryContext` span contract. + +RPC cancellation and telemetry propagation are independent control-plane channels: + +- trace metadata reconstructs telemetry parentage; +- request ID plus cancel/disconnect messages controls the server request signal; +- neither channel appears in serialized method arguments. + +## Interface migration scaffolding + +`TODO_CONTEXT` is a temporary migration marker, not a semantic root. Use it where a caller has not yet been taught which context to propagate. `BACKGROUND_CONTEXT` means intentionally start without a caller. + +TypeScript permits an implementation method to accept fewer parameters than its interface. Therefore changing public interfaces alone does not find every implementation or concrete-class call. The migration must: + +- update concrete method signatures explicitly, even when they temporarily ignore `_context`; +- inventory concrete calls, callback adapters, and object-literal façades; +- grep or check `TODO_CONTEXT` separately; +- eventually remove every unapproved `TODO_CONTEXT` use. + +This migration can make the repository compile before semantics are implemented. Compilation does not prove telemetry or cancellation correctness. + +## Required tests for the later handoff + +- immutable typed-value layering and key isolation; +- never-aborted background signal; +- crossed concurrent telemetry branches on one shared receiver; +- nested hook, tool, event-handler, and session-write parentage; +- buffered events retain their emitting context; +- no receiver-level telemetry default; +- pre-aborted invocation starts no external effect; +- installer and joiner cancellation isolation; +- last-waiter/owner policy selected by the final design; +- invocation abort leaves resumable durable state; +- durable abort commits the durable aborted outcome; +- close and disconnect do not masquerade as durable cancellation; +- client → server trace reconstruction; +- event delivery reconstructs source trace metadata; +- missing/malformed trace carriers degrade to no-op/root telemetry without affecting business behavior. + +## Open decisions before `harness.md` + +- final type and field names; +- context argument position; +- runtime brand and public visibility; +- installer-owned, lease-owned, or harness-owned drive execution; +- whether telemetry links are required for joiners; +- trace-carrier adapter ownership and shape; +- which context values, if any, may cross an RPC boundary; +- exact span names/outcome attributes for RPC calls and drive join waits.