Side-effect ownership
Extensions produce three materially different kinds of side effects. Each has exactly one official routing path; mixing them up is the most common package-authoring bug.
| Class | Examples | Owned by | Teardown |
|---|---|---|---|
| AppHost-local | event subscriptions, timers, watchers, in-memory caches | your extension’s lifecycle scope | reverse registration order, idempotent, async-safe |
| Kernel resource | workspaces, processes, containers, remote executions, ACP sessions | the kernel, durably, with recovery | kernel RPC (workspace.cleanup, execution.terminate, …) |
| External | messages, pushes, merges, approvals | nobody; they happened | compensation is a new command, never undo |
AppHost-local effects
Section titled “AppHost-local effects”Own every reversible effect on the activation lifecycle scope. A failed setup is cleaned automatically: effects registered before the failure never leak.
register: (context, lifecycle) => { lifecycle.scope.own(context.onEvent((event) => console.log(event.eventType))); const timer = setInterval(() => sync(), 60_000); lifecycle.scope.own(() => clearInterval(timer));};Kernel-owned resources
Section titled “Kernel-owned resources”Workspaces, processes, containers, remote executions, and ACP sessions survive AppHost crashes.
They are created, observed, and released through kernel RPC operations declared as
workspace/execution strategy definitions — never through a scope disposer. The AppHost-local
scope has no API for them, so there is nothing to misuse: resource recovery is the kernel’s job.
Containers and remote executions are execution strategies and route through execution.*.
Externally visible operations
Section titled “Externally visible operations”Messages, pushes, merges, and approvals are durable and irreversible. Route them through
context.propose with an idempotency key, and declare the capability you need:
lifecycle.scope.own( context.onEvent(async (event) => { await context.propose({ commandId: crypto.randomUUID(), commandType: "github.merge", idempotencyKey: `merge-${event.eventId}`, payload: { pull: event.payload.pull }, }); }),);The kernel deduplicates by idempotency key, records causation and audit provenance, and enforces
the capability grant. If the merge must later be compensated, propose a new command (for example
github.revert) with its own idempotency key. Never model compensation as an undo of the original
command: the original audit record stays intact.
Enforcement
Section titled “Enforcement”- The lifecycle scope exposes no kernel-resource or external authority surface (SDK shape check).
verifyOwnershipRoutingfrom the SDK runs a fixture through all three paths and fails deterministically when a kernel-resource operation is routed anywhere else, an external operation lacks an idempotency key or declares an unknown capability, or the scope exposes unexpected authority.
The canonical contract is the SDK recipe
sdk/recipes/ownership.md
and the ownership SDK reference.