Skip to content

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.

ClassExamplesOwned byTeardown
AppHost-localevent subscriptions, timers, watchers, in-memory cachesyour extension’s lifecycle scopereverse registration order, idempotent, async-safe
Kernel resourceworkspaces, processes, containers, remote executions, ACP sessionsthe kernel, durably, with recoverykernel RPC (workspace.cleanup, execution.terminate, …)
Externalmessages, pushes, merges, approvalsnobody; they happenedcompensation is a new command, never undo

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));
};

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.*.

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.

  • The lifecycle scope exposes no kernel-resource or external authority surface (SDK shape check).
  • verifyOwnershipRouting from 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.